prisma-sharding 0.0.2 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +177 -27
- package/dist/cli/migrate.js +117 -0
- package/dist/cli/studio.js +112 -0
- package/dist/cli/test.js +122 -0
- package/dist/index.js +101 -101
- package/dist/index.mjs +101 -101
- package/package.json +15 -3
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Prisma Sharding
|
|
2
2
|
|
|
3
|
-
Lightweight database sharding library for Prisma with connection pooling, health monitoring, and
|
|
3
|
+
Lightweight database sharding library for Prisma with connection pooling, health monitoring, and CLI tools.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -10,9 +10,13 @@ yarn add prisma-sharding
|
|
|
10
10
|
npm install prisma-sharding
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
> Don't forget to follow me on [GitHub](https://github.com/safdar-azeem)!
|
|
14
|
+
|
|
15
|
+
## Step 1: Create Sharding Connection
|
|
14
16
|
|
|
15
17
|
```typescript
|
|
18
|
+
// src/config/prisma.ts
|
|
19
|
+
|
|
16
20
|
import { PrismaSharding } from 'prisma-sharding';
|
|
17
21
|
import { PrismaClient } from '@/generated/prisma';
|
|
18
22
|
import { PrismaPg } from '@prisma/adapter-pg';
|
|
@@ -21,7 +25,6 @@ const sharding = new PrismaSharding<PrismaClient>({
|
|
|
21
25
|
shards: [
|
|
22
26
|
{ id: 'shard_1', url: process.env.SHARD_1_URL! },
|
|
23
27
|
{ id: 'shard_2', url: process.env.SHARD_2_URL! },
|
|
24
|
-
{ id: 'shard_3', url: process.env.SHARD_3_URL! },
|
|
25
28
|
],
|
|
26
29
|
strategy: 'modulo', // 'modulo' | 'consistent-hash'
|
|
27
30
|
createClient: (url) => {
|
|
@@ -30,32 +33,78 @@ const sharding = new PrismaSharding<PrismaClient>({
|
|
|
30
33
|
},
|
|
31
34
|
});
|
|
32
35
|
|
|
33
|
-
// Initialize connections
|
|
34
36
|
await sharding.connect();
|
|
35
37
|
```
|
|
36
38
|
|
|
37
|
-
##
|
|
39
|
+
## API
|
|
38
40
|
|
|
39
|
-
|
|
41
|
+
| Method | Description |
|
|
42
|
+
| ---------------------------- | --------------------------------------------- |
|
|
43
|
+
| `getShard(key)` | Get Prisma client for a given key |
|
|
44
|
+
| `getShardById(shardId)` | Get Prisma client by shard ID |
|
|
45
|
+
| `getRandomShard()` | Get random shard (for new records) |
|
|
46
|
+
| `findFirst(fn)` | Search across all shards, return first result |
|
|
47
|
+
| `runOnAll(fn)` | Execute on all shards |
|
|
48
|
+
| `getHealth()` | Get health status of all shards |
|
|
49
|
+
| `connect()` / `disconnect()` | Lifecycle methods |
|
|
50
|
+
|
|
51
|
+
### Step 2: Create a User (Assign to a Shard)
|
|
52
|
+
|
|
53
|
+
New records should be created on a random shard for even distribution.
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
import { sharding } from '@/config/prisma';
|
|
57
|
+
|
|
58
|
+
const client = sharding.getRandomShard();
|
|
59
|
+
|
|
60
|
+
const user = await client.user.create({
|
|
61
|
+
data: {
|
|
62
|
+
email: 'user@example.com',
|
|
63
|
+
username: 'new_user',
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Step 3: Access User by ID (Shard Routing)
|
|
69
|
+
|
|
70
|
+
When you have a user ID, Prisma Sharding routes you to the correct shard automatically.
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
const userId = 'abc123';
|
|
40
74
|
|
|
41
|
-
```typescript
|
|
42
|
-
// Get client for existing user (routed by user ID)
|
|
43
75
|
const client = sharding.getShard(userId);
|
|
44
|
-
const user = await client.user.findUnique({ where: { id: userId } });
|
|
45
76
|
|
|
46
|
-
|
|
47
|
-
|
|
77
|
+
const user = await client.user.findUnique({
|
|
78
|
+
where: { id: userId },
|
|
79
|
+
});
|
|
48
80
|
```
|
|
49
81
|
|
|
50
|
-
|
|
82
|
+
`Important rule:`
|
|
51
83
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
84
|
+
Once you get the shard client using a user ID, **all future operations for that user must use this same client**.
|
|
85
|
+
|
|
86
|
+
That includes:
|
|
87
|
+
|
|
88
|
+
- Reading user data
|
|
89
|
+
- Updating user data
|
|
90
|
+
- Creating related records (profiles, posts, settings, etc)
|
|
91
|
+
|
|
92
|
+
Every user belongs to exactly one shard. Their entire data lives on that shard only.
|
|
93
|
+
|
|
94
|
+
Do **not** switch shards or use a random shard for user related actions.
|
|
95
|
+
|
|
96
|
+
Always do this:
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
const client = sharding.getShard(userId);
|
|
56
100
|
```
|
|
57
101
|
|
|
58
|
-
|
|
102
|
+
This guarantees all user data stays on the correct shard and avoids cross shard bugs.
|
|
103
|
+
|
|
104
|
+
### Step 4: Find User Without ID (Cross Shard Search)
|
|
105
|
+
|
|
106
|
+
If you do not have the user ID, search all shards in parallel.
|
|
107
|
+
Use this only when necessary.
|
|
59
108
|
|
|
60
109
|
```typescript
|
|
61
110
|
// Find user by email across ALL shards (parallel execution)
|
|
@@ -72,7 +121,7 @@ if (user && client) {
|
|
|
72
121
|
}
|
|
73
122
|
```
|
|
74
123
|
|
|
75
|
-
|
|
124
|
+
## Step 5: Run on All Shards (Admin or Analytics)
|
|
76
125
|
|
|
77
126
|
```typescript
|
|
78
127
|
// Get counts from all shards
|
|
@@ -108,16 +157,71 @@ if (sharding.isConnected()) {
|
|
|
108
157
|
}
|
|
109
158
|
```
|
|
110
159
|
|
|
160
|
+
## CLI Tools
|
|
161
|
+
|
|
162
|
+
The package includes CLI tools for common sharding operations. No need to write custom scripts!
|
|
163
|
+
|
|
164
|
+
### Setup
|
|
165
|
+
|
|
166
|
+
Add to your `package.json`:
|
|
167
|
+
|
|
168
|
+
```json
|
|
169
|
+
{
|
|
170
|
+
"scripts": {
|
|
171
|
+
"db:studio:all": "prisma-sharding-studio",
|
|
172
|
+
"migrate:shards": "prisma-sharding-migrate",
|
|
173
|
+
"test:shards": "prisma-sharding-test"
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Environment Variables
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
SHARD_COUNT=3
|
|
182
|
+
SHARD_1_URL=postgresql://user:pass@host:5432/db1
|
|
183
|
+
SHARD_2_URL=postgresql://user:pass@host:5432/db2
|
|
184
|
+
SHARD_3_URL=postgresql://user:pass@host:5432/db3
|
|
185
|
+
SHARD_ROUTING_STRATEGY=modulo # or consistent-hash
|
|
186
|
+
SHARD_STUDIO_BASE_PORT=51212 # optional, for studio
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Commands
|
|
190
|
+
|
|
191
|
+
#### `prisma-sharding-migrate`
|
|
192
|
+
|
|
193
|
+
Push schema to all shards using `prisma db push`.
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
yarn migrate:shards
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
#### `prisma-sharding-studio`
|
|
200
|
+
|
|
201
|
+
Start Prisma Studio for all shards on sequential ports.
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
yarn db:studio:all
|
|
205
|
+
# Opens shard_1 on :51212, shard_2 on :51213, etc.
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
#### `prisma-sharding-test`
|
|
209
|
+
|
|
210
|
+
Test connections to all shards.
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
yarn test:shards
|
|
214
|
+
```
|
|
215
|
+
|
|
111
216
|
## Configuration
|
|
112
217
|
|
|
113
|
-
| Option | Type | Default | Description
|
|
114
|
-
| ------------------------- | ------------------------------- | ---------- |
|
|
115
|
-
| `shards` | `ShardConfig[]` | Required | Array of shard configurations
|
|
116
|
-
| `strategy` | `'modulo' \| 'consistent-hash'` | `'modulo'` | Routing algorithm
|
|
117
|
-
| `createClient` | `(url, shardId) => TClient` | Required | Factory
|
|
118
|
-
| `healthCheckIntervalMs` | `number` | `30000` | Health check frequency
|
|
119
|
-
| `circuitBreakerThreshold` | `number` | `3` | Failures before marking unhealthy
|
|
120
|
-
| `logger` | `ShardingLogger` | Console | Custom logger |
|
|
218
|
+
| Option | Type | Default | Description |
|
|
219
|
+
| ------------------------- | ------------------------------- | ---------- | --------------------------------- |
|
|
220
|
+
| `shards` | `ShardConfig[]` | Required | Array of shard configurations |
|
|
221
|
+
| `strategy` | `'modulo' \| 'consistent-hash'` | `'modulo'` | Routing algorithm |
|
|
222
|
+
| `createClient` | `(url, shardId) => TClient` | Required | Factory to create Prisma clients |
|
|
223
|
+
| `healthCheckIntervalMs` | `number` | `30000` | Health check frequency |
|
|
224
|
+
| `circuitBreakerThreshold` | `number` | `3` | Failures before marking unhealthy |
|
|
121
225
|
|
|
122
226
|
### Shard Config
|
|
123
227
|
|
|
@@ -151,7 +255,7 @@ strategy: 'consistent-hash';
|
|
|
151
255
|
## Error Handling
|
|
152
256
|
|
|
153
257
|
```typescript
|
|
154
|
-
import { ShardingError, ConfigError, ConnectionError
|
|
258
|
+
import { ShardingError, ConfigError, ConnectionError } from 'prisma-sharding';
|
|
155
259
|
|
|
156
260
|
try {
|
|
157
261
|
const client = sharding.getShard(userId);
|
|
@@ -175,6 +279,52 @@ const sharding = new PrismaSharding({
|
|
|
175
279
|
});
|
|
176
280
|
```
|
|
177
281
|
|
|
282
|
+
---
|
|
283
|
+
|
|
284
|
+
### `getAllClients()`
|
|
285
|
+
|
|
286
|
+
Get all Prisma client instances.
|
|
287
|
+
|
|
288
|
+
```typescript
|
|
289
|
+
const clients = sharding.getAllClients();
|
|
290
|
+
|
|
291
|
+
console.log(`Managing ${clients.length} shard clients`);
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
**Returns:** `PrismaClient[]`
|
|
295
|
+
|
|
296
|
+
---
|
|
297
|
+
|
|
298
|
+
### `getShardCount()`
|
|
299
|
+
|
|
300
|
+
Get total number of configured shards.
|
|
301
|
+
|
|
302
|
+
```typescript
|
|
303
|
+
const count = sharding.getShardCount();
|
|
304
|
+
console.log(`Running on ${count} shards`);
|
|
305
|
+
// Output: Running on 3 shards
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
---
|
|
309
|
+
|
|
310
|
+
### `getShardIds()`
|
|
311
|
+
|
|
312
|
+
Get array of all shard IDs.
|
|
313
|
+
|
|
314
|
+
```typescript
|
|
315
|
+
const shardIds = sharding.getShardIds();
|
|
316
|
+
console.log(shardIds);
|
|
317
|
+
// Output: ['shard_1', 'shard_2', 'shard_3']
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
**Returns:** `string[]`
|
|
321
|
+
|
|
322
|
+
---
|
|
323
|
+
|
|
324
|
+
## Author
|
|
325
|
+
|
|
326
|
+
[safdar-azeem](https://github.com/safdar-azeem)
|
|
327
|
+
|
|
178
328
|
## License
|
|
179
329
|
|
|
180
330
|
MIT
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
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
|
+
));
|
|
25
|
+
|
|
26
|
+
// src/cli/migrate.ts
|
|
27
|
+
var import_config = require("dotenv/config");
|
|
28
|
+
var import_child_process = require("child_process");
|
|
29
|
+
var import_path = __toESM(require("path"));
|
|
30
|
+
var getShardConfigs = () => {
|
|
31
|
+
const shards = [];
|
|
32
|
+
const shardCount = parseInt(process.env.SHARD_COUNT || "0", 10);
|
|
33
|
+
for (let i = 1; i <= shardCount; i++) {
|
|
34
|
+
const url = process.env[`SHARD_${i}_URL`];
|
|
35
|
+
if (url) {
|
|
36
|
+
shards.push({ id: `shard_${i}`, url });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (shards.length === 0 && process.env.DATABASE_URL) {
|
|
40
|
+
shards.push({ id: "shard_1", url: process.env.DATABASE_URL });
|
|
41
|
+
}
|
|
42
|
+
return shards;
|
|
43
|
+
};
|
|
44
|
+
var runPrismaCommand = (shardUrl, command) => {
|
|
45
|
+
return new Promise((resolve) => {
|
|
46
|
+
const prisma = (0, import_child_process.spawn)("npx", ["prisma", ...command, "--url", shardUrl], {
|
|
47
|
+
env: process.env,
|
|
48
|
+
cwd: import_path.default.resolve(process.cwd()),
|
|
49
|
+
shell: true
|
|
50
|
+
});
|
|
51
|
+
let output = "";
|
|
52
|
+
let errorOutput = "";
|
|
53
|
+
prisma.stdout.on("data", (data) => {
|
|
54
|
+
output += data.toString();
|
|
55
|
+
});
|
|
56
|
+
prisma.stderr.on("data", (data) => {
|
|
57
|
+
errorOutput += data.toString();
|
|
58
|
+
});
|
|
59
|
+
prisma.on("close", (code) => {
|
|
60
|
+
if (code === 0) {
|
|
61
|
+
resolve({ output });
|
|
62
|
+
} else {
|
|
63
|
+
resolve({ output, error: errorOutput || `Exit code: ${code}` });
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
prisma.on("error", (err) => {
|
|
67
|
+
resolve({ output, error: err.message });
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
};
|
|
71
|
+
var migrateAllShards = async () => {
|
|
72
|
+
const shards = getShardConfigs();
|
|
73
|
+
console.log("\u{1F504} prisma-sharding: Starting migrations...\n");
|
|
74
|
+
console.log(`\u{1F4CA} Total shards to migrate: ${shards.length}
|
|
75
|
+
`);
|
|
76
|
+
if (shards.length === 0) {
|
|
77
|
+
console.error(
|
|
78
|
+
"\u274C No shards configured. Set SHARD_COUNT and SHARD_N_URL environment variables."
|
|
79
|
+
);
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
const results = [];
|
|
83
|
+
for (const shard of shards) {
|
|
84
|
+
console.log(`
|
|
85
|
+
\u{1F4E6} Migrating ${shard.id}...`);
|
|
86
|
+
console.log(` URL: ${shard.url.replace(/:[^:@]+@/, ":***@")}`);
|
|
87
|
+
const { output, error } = await runPrismaCommand(shard.url, ["db", "push"]);
|
|
88
|
+
if (error && !output.includes("Your database is now in sync")) {
|
|
89
|
+
console.error(` \u274C Failed: ${error.split("\n")[0]}`);
|
|
90
|
+
results.push({ shardId: shard.id, success: false, output, error });
|
|
91
|
+
} else {
|
|
92
|
+
console.log(` \u2705 Success`);
|
|
93
|
+
results.push({ shardId: shard.id, success: true, output });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
console.log("\n" + "=".repeat(50));
|
|
97
|
+
console.log("\u{1F4CB} Migration Summary\n");
|
|
98
|
+
const successful = results.filter((r) => r.success).length;
|
|
99
|
+
const failed = results.filter((r) => !r.success).length;
|
|
100
|
+
results.forEach((result) => {
|
|
101
|
+
const status = result.success ? "\u2705" : "\u274C";
|
|
102
|
+
console.log(
|
|
103
|
+
` ${status} ${result.shardId}${result.error ? ` - ${result.error.split("\n")[0]}` : ""}`
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
console.log(`
|
|
107
|
+
Total: ${results.length} | Success: ${successful} | Failed: ${failed}`);
|
|
108
|
+
if (failed > 0) {
|
|
109
|
+
console.log("\n\u26A0\uFE0F Some migrations failed. Please review the errors above.");
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
console.log("\n\u2705 All shard migrations completed successfully!");
|
|
113
|
+
};
|
|
114
|
+
migrateAllShards().catch((error) => {
|
|
115
|
+
console.error("Migration script failed:", error);
|
|
116
|
+
process.exit(1);
|
|
117
|
+
});
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
// src/cli/studio.ts
|
|
5
|
+
var import_config = require("dotenv/config");
|
|
6
|
+
var import_child_process = require("child_process");
|
|
7
|
+
var instances = [];
|
|
8
|
+
var BASE_PORT = parseInt(process.env.SHARD_STUDIO_BASE_PORT || "51212", 10);
|
|
9
|
+
var getShardConfigs = () => {
|
|
10
|
+
const shards = [];
|
|
11
|
+
const shardCount = parseInt(process.env.SHARD_COUNT || "0", 10);
|
|
12
|
+
for (let i = 1; i <= shardCount; i++) {
|
|
13
|
+
const url = process.env[`SHARD_${i}_URL`];
|
|
14
|
+
if (url) {
|
|
15
|
+
shards.push({ id: `shard_${i}`, url });
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
if (shards.length === 0 && process.env.DATABASE_URL) {
|
|
19
|
+
shards.push({ id: "shard_1", url: process.env.DATABASE_URL });
|
|
20
|
+
}
|
|
21
|
+
return shards;
|
|
22
|
+
};
|
|
23
|
+
var startStudio = (shard, index) => {
|
|
24
|
+
return new Promise((resolve, reject) => {
|
|
25
|
+
const port = BASE_PORT + index;
|
|
26
|
+
const shardId = shard.id;
|
|
27
|
+
console.log(`
|
|
28
|
+
\u{1F680} Starting Prisma Studio for ${shardId} on port ${port}...`);
|
|
29
|
+
console.log(` URL: ${shard.url.replace(/:[^:@]+@/, ":***@")}`);
|
|
30
|
+
const studioProcess = (0, import_child_process.spawn)(
|
|
31
|
+
"npx",
|
|
32
|
+
["prisma", "studio", "--port", port.toString(), "--browser", "none"],
|
|
33
|
+
{
|
|
34
|
+
env: {
|
|
35
|
+
...process.env,
|
|
36
|
+
DATABASE_URL: shard.url
|
|
37
|
+
},
|
|
38
|
+
shell: true,
|
|
39
|
+
stdio: "pipe"
|
|
40
|
+
}
|
|
41
|
+
);
|
|
42
|
+
studioProcess.stdout?.on("data", (data) => {
|
|
43
|
+
const output = data.toString();
|
|
44
|
+
if (output.includes("Prisma Studio is running")) {
|
|
45
|
+
console.log(` \u2705 ${shardId} ready at http://localhost:${port}`);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
studioProcess.stderr?.on("data", (data) => {
|
|
49
|
+
const output = data.toString();
|
|
50
|
+
if (!output.includes("warn") && !output.includes("Loaded")) {
|
|
51
|
+
console.error(` [${shardId}] ${output}`);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
studioProcess.on("error", (err) => {
|
|
55
|
+
console.error(` \u274C Failed to start ${shardId}:`, err.message);
|
|
56
|
+
reject(err);
|
|
57
|
+
});
|
|
58
|
+
const instance = {
|
|
59
|
+
shardId,
|
|
60
|
+
port,
|
|
61
|
+
process: studioProcess
|
|
62
|
+
};
|
|
63
|
+
instances.push(instance);
|
|
64
|
+
setTimeout(() => resolve(instance), 2e3);
|
|
65
|
+
});
|
|
66
|
+
};
|
|
67
|
+
var startAllStudios = async () => {
|
|
68
|
+
const shards = getShardConfigs();
|
|
69
|
+
console.log("=".repeat(60));
|
|
70
|
+
console.log("\u{1F5C4}\uFE0F prisma-sharding: Multi-Shard Studio Viewer");
|
|
71
|
+
console.log("=".repeat(60));
|
|
72
|
+
console.log(`
|
|
73
|
+
\u{1F4CA} Starting ${shards.length} Prisma Studio instance(s)...
|
|
74
|
+
`);
|
|
75
|
+
if (shards.length === 0) {
|
|
76
|
+
console.error(
|
|
77
|
+
"\u274C No shards configured. Set SHARD_COUNT and SHARD_N_URL environment variables."
|
|
78
|
+
);
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
for (let i = 0; i < shards.length; i++) {
|
|
82
|
+
try {
|
|
83
|
+
await startStudio(shards[i], i);
|
|
84
|
+
} catch (error) {
|
|
85
|
+
console.error(`Failed to start studio for ${shards[i].id}:`, error);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
console.log("\n" + "=".repeat(60));
|
|
89
|
+
console.log("\u{1F4CB} All Studios Running:");
|
|
90
|
+
console.log("=".repeat(60));
|
|
91
|
+
instances.forEach((instance) => {
|
|
92
|
+
console.log(` \u2022 ${instance.shardId}: http://localhost:${instance.port}`);
|
|
93
|
+
});
|
|
94
|
+
console.log("\n Press Ctrl+C to stop all instances\n");
|
|
95
|
+
};
|
|
96
|
+
var gracefulShutdown = () => {
|
|
97
|
+
console.log("\n\n\u{1F6D1} Shutting down all Prisma Studio instances...\n");
|
|
98
|
+
instances.forEach((instance) => {
|
|
99
|
+
console.log(` Stopping ${instance.shardId}...`);
|
|
100
|
+
instance.process.kill("SIGTERM");
|
|
101
|
+
});
|
|
102
|
+
setTimeout(() => {
|
|
103
|
+
console.log("\n\u2705 All instances stopped\n");
|
|
104
|
+
process.exit(0);
|
|
105
|
+
}, 1e3);
|
|
106
|
+
};
|
|
107
|
+
process.on("SIGINT", gracefulShutdown);
|
|
108
|
+
process.on("SIGTERM", gracefulShutdown);
|
|
109
|
+
startAllStudios().catch((error) => {
|
|
110
|
+
console.error("Failed to start studios:", error);
|
|
111
|
+
process.exit(1);
|
|
112
|
+
});
|
package/dist/cli/test.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
// src/cli/test.ts
|
|
5
|
+
var import_config = require("dotenv/config");
|
|
6
|
+
var import_child_process = require("child_process");
|
|
7
|
+
var results = [];
|
|
8
|
+
var log = {
|
|
9
|
+
info: (msg) => console.log(`\u2139\uFE0F ${msg}`),
|
|
10
|
+
success: (msg) => console.log(`\u2705 ${msg}`),
|
|
11
|
+
error: (msg) => console.log(`\u274C ${msg}`),
|
|
12
|
+
warn: (msg) => console.log(`\u26A0\uFE0F ${msg}`),
|
|
13
|
+
section: (msg) => console.log(`
|
|
14
|
+
${"=".repeat(50)}
|
|
15
|
+
\u{1F4CB} ${msg}
|
|
16
|
+
${"=".repeat(50)}`)
|
|
17
|
+
};
|
|
18
|
+
var getShardConfigs = () => {
|
|
19
|
+
const shards = [];
|
|
20
|
+
const shardCount = parseInt(process.env.SHARD_COUNT || "0", 10);
|
|
21
|
+
for (let i = 1; i <= shardCount; i++) {
|
|
22
|
+
const url = process.env[`SHARD_${i}_URL`];
|
|
23
|
+
if (url) {
|
|
24
|
+
shards.push({ id: `shard_${i}`, url });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (shards.length === 0 && process.env.DATABASE_URL) {
|
|
28
|
+
shards.push({ id: "shard_1", url: process.env.DATABASE_URL });
|
|
29
|
+
}
|
|
30
|
+
return shards;
|
|
31
|
+
};
|
|
32
|
+
var runTest = async (name, testFn) => {
|
|
33
|
+
const start = Date.now();
|
|
34
|
+
try {
|
|
35
|
+
await testFn();
|
|
36
|
+
const duration = Date.now() - start;
|
|
37
|
+
results.push({ name, passed: true, message: "Passed", duration });
|
|
38
|
+
log.success(`${name} (${duration}ms)`);
|
|
39
|
+
} catch (error) {
|
|
40
|
+
const duration = Date.now() - start;
|
|
41
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
42
|
+
results.push({ name, passed: false, message, duration });
|
|
43
|
+
log.error(`${name}: ${message}`);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
var testConnection = (url) => {
|
|
47
|
+
return new Promise((resolve) => {
|
|
48
|
+
const prisma = (0, import_child_process.spawn)("npx", ["prisma", "db", "execute", "--url", url, "--stdin"], {
|
|
49
|
+
shell: true,
|
|
50
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
51
|
+
});
|
|
52
|
+
prisma.stdin.write("SELECT 1");
|
|
53
|
+
prisma.stdin.end();
|
|
54
|
+
prisma.on("close", (code) => {
|
|
55
|
+
resolve(code === 0);
|
|
56
|
+
});
|
|
57
|
+
prisma.on("error", () => {
|
|
58
|
+
resolve(false);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
};
|
|
62
|
+
var runTests = async () => {
|
|
63
|
+
const shards = getShardConfigs();
|
|
64
|
+
console.log("\n\u{1F9EA} prisma-sharding: Shard Connection Test Suite\n");
|
|
65
|
+
console.log(`\u{1F4CA} Configuration:`);
|
|
66
|
+
console.log(` - Shard Count: ${shards.length}`);
|
|
67
|
+
console.log(` - Routing Strategy: ${process.env.SHARD_ROUTING_STRATEGY || "modulo"}`);
|
|
68
|
+
if (shards.length === 0) {
|
|
69
|
+
console.error(
|
|
70
|
+
"\n\u274C No shards configured. Set SHARD_COUNT and SHARD_N_URL environment variables."
|
|
71
|
+
);
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
log.section("Test 1: Shard Connection Tests");
|
|
75
|
+
for (const shard of shards) {
|
|
76
|
+
await runTest(`Connect to ${shard.id}`, async () => {
|
|
77
|
+
const success = await testConnection(shard.url);
|
|
78
|
+
if (!success) {
|
|
79
|
+
throw new Error(`Failed to connect to ${shard.id}`);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
log.section("Test 2: Configuration Verification");
|
|
84
|
+
await runTest("Verify environment variables", async () => {
|
|
85
|
+
const shardCount = parseInt(process.env.SHARD_COUNT || "0", 10);
|
|
86
|
+
if (shardCount === 0 && !process.env.DATABASE_URL) {
|
|
87
|
+
throw new Error("SHARD_COUNT or DATABASE_URL must be set");
|
|
88
|
+
}
|
|
89
|
+
log.info(`SHARD_COUNT: ${shardCount}`);
|
|
90
|
+
log.info(`Routing Strategy: ${process.env.SHARD_ROUTING_STRATEGY || "modulo"}`);
|
|
91
|
+
});
|
|
92
|
+
await runTest("Verify all shard URLs are present", async () => {
|
|
93
|
+
const shardCount = parseInt(process.env.SHARD_COUNT || "0", 10);
|
|
94
|
+
for (let i = 1; i <= shardCount; i++) {
|
|
95
|
+
const url = process.env[`SHARD_${i}_URL`];
|
|
96
|
+
if (!url) {
|
|
97
|
+
throw new Error(`SHARD_${i}_URL is missing`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
log.section("Test Results Summary");
|
|
102
|
+
const passed = results.filter((r) => r.passed).length;
|
|
103
|
+
const failed = results.filter((r) => !r.passed).length;
|
|
104
|
+
const totalDuration = results.reduce((sum, r) => sum + r.duration, 0);
|
|
105
|
+
console.log(`
|
|
106
|
+
Total Tests: ${results.length}`);
|
|
107
|
+
console.log(` Passed: ${passed}`);
|
|
108
|
+
console.log(` Failed: ${failed}`);
|
|
109
|
+
console.log(` Duration: ${totalDuration}ms`);
|
|
110
|
+
if (failed > 0) {
|
|
111
|
+
console.log("\n Failed Tests:");
|
|
112
|
+
results.filter((r) => !r.passed).forEach((r) => {
|
|
113
|
+
console.log(` - ${r.name}: ${r.message}`);
|
|
114
|
+
});
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
console.log("\n\u2705 All shard tests passed!");
|
|
118
|
+
};
|
|
119
|
+
runTests().catch((error) => {
|
|
120
|
+
console.error("\n\u{1F4A5} Test suite crashed:", error);
|
|
121
|
+
process.exit(1);
|
|
122
|
+
});
|
package/dist/index.js
CHANGED
|
@@ -33,25 +33,26 @@ __export(index_exports, {
|
|
|
33
33
|
});
|
|
34
34
|
module.exports = __toCommonJS(index_exports);
|
|
35
35
|
|
|
36
|
-
// src/
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
};
|
|
36
|
+
// src/utils/index.ts
|
|
37
|
+
function hashString(str) {
|
|
38
|
+
let hash = 0;
|
|
39
|
+
for (let i = 0; i < str.length; i++) {
|
|
40
|
+
const char = str.charCodeAt(i);
|
|
41
|
+
hash = (hash << 5) - hash + char;
|
|
42
|
+
hash = hash & hash;
|
|
43
|
+
}
|
|
44
|
+
return Math.abs(hash);
|
|
45
|
+
}
|
|
46
|
+
function validateUrl(url) {
|
|
47
|
+
return url.startsWith("postgresql://") || url.startsWith("postgres://");
|
|
48
|
+
}
|
|
49
|
+
function createDefaultLogger() {
|
|
50
|
+
return {
|
|
51
|
+
info: (msg) => console.log(`[PrismaSharding] ${msg}`),
|
|
52
|
+
warn: (msg) => console.warn(`[PrismaSharding] ${msg}`),
|
|
53
|
+
error: (msg) => console.error(`[PrismaSharding] ${msg}`)
|
|
54
|
+
};
|
|
55
|
+
}
|
|
55
56
|
|
|
56
57
|
// src/core/errors.ts
|
|
57
58
|
var ShardingError = class _ShardingError extends Error {
|
|
@@ -85,6 +86,87 @@ var RoutingError = class _RoutingError extends ShardingError {
|
|
|
85
86
|
}
|
|
86
87
|
};
|
|
87
88
|
|
|
89
|
+
// src/constants/index.ts
|
|
90
|
+
var DEFAULTS = {
|
|
91
|
+
POOL_MAX_CONNECTIONS: 10,
|
|
92
|
+
POOL_IDLE_TIMEOUT_MS: 1e4,
|
|
93
|
+
POOL_CONNECTION_TIMEOUT_MS: 5e3,
|
|
94
|
+
HEALTH_CHECK_INTERVAL_MS: 3e4,
|
|
95
|
+
CIRCUIT_BREAKER_THRESHOLD: 3,
|
|
96
|
+
CONSISTENT_HASH_VIRTUAL_NODES: 150
|
|
97
|
+
};
|
|
98
|
+
var ERROR_MESSAGES = {
|
|
99
|
+
NO_SHARDS: "At least one shard must be configured",
|
|
100
|
+
SHARD_NOT_FOUND: (id) => `Shard "${id}" not found`,
|
|
101
|
+
NO_HEALTHY_SHARDS: "No healthy shards available",
|
|
102
|
+
INVALID_STRATEGY: (s) => `Invalid routing strategy: "${s}". Use "modulo" or "consistent-hash"`,
|
|
103
|
+
NOT_CONNECTED: "Sharding not connected. Call connect() first",
|
|
104
|
+
ALREADY_CONNECTED: "Sharding already connected",
|
|
105
|
+
MISSING_CLIENT_FACTORY: "createClient function is required",
|
|
106
|
+
INVALID_SHARD_URL: (id) => `Invalid or missing URL for shard "${id}"`
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
// src/core/router.ts
|
|
110
|
+
var ShardRouter = class {
|
|
111
|
+
constructor(config) {
|
|
112
|
+
this.consistentHashRing = /* @__PURE__ */ new Map();
|
|
113
|
+
this.virtualNodes = DEFAULTS.CONSISTENT_HASH_VIRTUAL_NODES;
|
|
114
|
+
this.strategy = config.strategy;
|
|
115
|
+
this.shardIds = config.shardIds;
|
|
116
|
+
this.logger = config.logger;
|
|
117
|
+
if (this.strategy === "consistent-hash") {
|
|
118
|
+
this.initializeConsistentHashRing();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
initializeConsistentHashRing() {
|
|
122
|
+
for (const shardId of this.shardIds) {
|
|
123
|
+
for (let i = 0; i < this.virtualNodes; i++) {
|
|
124
|
+
const hash = hashString(`${shardId}:${i}`);
|
|
125
|
+
this.consistentHashRing.set(hash, shardId);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
getShardIndex(key) {
|
|
130
|
+
const shardCount = this.shardIds.length;
|
|
131
|
+
if (shardCount === 0) {
|
|
132
|
+
throw new RoutingError("No shards available");
|
|
133
|
+
}
|
|
134
|
+
if (this.strategy === "consistent-hash") {
|
|
135
|
+
return this.getIndexConsistentHash(key);
|
|
136
|
+
}
|
|
137
|
+
return this.getIndexModulo(key, shardCount);
|
|
138
|
+
}
|
|
139
|
+
getIndexModulo(key, shardCount) {
|
|
140
|
+
const hash = hashString(key);
|
|
141
|
+
return hash % shardCount;
|
|
142
|
+
}
|
|
143
|
+
getIndexConsistentHash(key) {
|
|
144
|
+
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;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const firstShardId = this.consistentHashRing.get(sortedHashes[0]);
|
|
154
|
+
const match = firstShardId.match(/shard_(\d+)/);
|
|
155
|
+
return match ? parseInt(match[1], 10) - 1 : 0;
|
|
156
|
+
}
|
|
157
|
+
getShardId(key) {
|
|
158
|
+
const index = this.getShardIndex(key);
|
|
159
|
+
return this.shardIds[index] || `shard_${index + 1}`;
|
|
160
|
+
}
|
|
161
|
+
getRandomShardIndex() {
|
|
162
|
+
return Math.floor(Math.random() * this.shardIds.length);
|
|
163
|
+
}
|
|
164
|
+
getRandomShardId() {
|
|
165
|
+
const index = this.getRandomShardIndex();
|
|
166
|
+
return this.shardIds[index];
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
|
|
88
170
|
// src/core/manager.ts
|
|
89
171
|
var ShardManager = class {
|
|
90
172
|
constructor(config) {
|
|
@@ -252,88 +334,6 @@ var ShardManager = class {
|
|
|
252
334
|
}
|
|
253
335
|
};
|
|
254
336
|
|
|
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
337
|
// src/core/sharding.ts
|
|
338
338
|
var PrismaSharding = class {
|
|
339
339
|
constructor(config) {
|
package/dist/index.mjs
CHANGED
|
@@ -1,22 +1,23 @@
|
|
|
1
|
-
// src/
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
};
|
|
1
|
+
// src/utils/index.ts
|
|
2
|
+
function hashString(str) {
|
|
3
|
+
let hash = 0;
|
|
4
|
+
for (let i = 0; i < str.length; i++) {
|
|
5
|
+
const char = str.charCodeAt(i);
|
|
6
|
+
hash = (hash << 5) - hash + char;
|
|
7
|
+
hash = hash & hash;
|
|
8
|
+
}
|
|
9
|
+
return Math.abs(hash);
|
|
10
|
+
}
|
|
11
|
+
function validateUrl(url) {
|
|
12
|
+
return url.startsWith("postgresql://") || url.startsWith("postgres://");
|
|
13
|
+
}
|
|
14
|
+
function createDefaultLogger() {
|
|
15
|
+
return {
|
|
16
|
+
info: (msg) => console.log(`[PrismaSharding] ${msg}`),
|
|
17
|
+
warn: (msg) => console.warn(`[PrismaSharding] ${msg}`),
|
|
18
|
+
error: (msg) => console.error(`[PrismaSharding] ${msg}`)
|
|
19
|
+
};
|
|
20
|
+
}
|
|
20
21
|
|
|
21
22
|
// src/core/errors.ts
|
|
22
23
|
var ShardingError = class _ShardingError extends Error {
|
|
@@ -50,6 +51,87 @@ var RoutingError = class _RoutingError extends ShardingError {
|
|
|
50
51
|
}
|
|
51
52
|
};
|
|
52
53
|
|
|
54
|
+
// src/constants/index.ts
|
|
55
|
+
var DEFAULTS = {
|
|
56
|
+
POOL_MAX_CONNECTIONS: 10,
|
|
57
|
+
POOL_IDLE_TIMEOUT_MS: 1e4,
|
|
58
|
+
POOL_CONNECTION_TIMEOUT_MS: 5e3,
|
|
59
|
+
HEALTH_CHECK_INTERVAL_MS: 3e4,
|
|
60
|
+
CIRCUIT_BREAKER_THRESHOLD: 3,
|
|
61
|
+
CONSISTENT_HASH_VIRTUAL_NODES: 150
|
|
62
|
+
};
|
|
63
|
+
var ERROR_MESSAGES = {
|
|
64
|
+
NO_SHARDS: "At least one shard must be configured",
|
|
65
|
+
SHARD_NOT_FOUND: (id) => `Shard "${id}" not found`,
|
|
66
|
+
NO_HEALTHY_SHARDS: "No healthy shards available",
|
|
67
|
+
INVALID_STRATEGY: (s) => `Invalid routing strategy: "${s}". Use "modulo" or "consistent-hash"`,
|
|
68
|
+
NOT_CONNECTED: "Sharding not connected. Call connect() first",
|
|
69
|
+
ALREADY_CONNECTED: "Sharding already connected",
|
|
70
|
+
MISSING_CLIENT_FACTORY: "createClient function is required",
|
|
71
|
+
INVALID_SHARD_URL: (id) => `Invalid or missing URL for shard "${id}"`
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// src/core/router.ts
|
|
75
|
+
var ShardRouter = class {
|
|
76
|
+
constructor(config) {
|
|
77
|
+
this.consistentHashRing = /* @__PURE__ */ new Map();
|
|
78
|
+
this.virtualNodes = DEFAULTS.CONSISTENT_HASH_VIRTUAL_NODES;
|
|
79
|
+
this.strategy = config.strategy;
|
|
80
|
+
this.shardIds = config.shardIds;
|
|
81
|
+
this.logger = config.logger;
|
|
82
|
+
if (this.strategy === "consistent-hash") {
|
|
83
|
+
this.initializeConsistentHashRing();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
initializeConsistentHashRing() {
|
|
87
|
+
for (const shardId of this.shardIds) {
|
|
88
|
+
for (let i = 0; i < this.virtualNodes; i++) {
|
|
89
|
+
const hash = hashString(`${shardId}:${i}`);
|
|
90
|
+
this.consistentHashRing.set(hash, shardId);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
getShardIndex(key) {
|
|
95
|
+
const shardCount = this.shardIds.length;
|
|
96
|
+
if (shardCount === 0) {
|
|
97
|
+
throw new RoutingError("No shards available");
|
|
98
|
+
}
|
|
99
|
+
if (this.strategy === "consistent-hash") {
|
|
100
|
+
return this.getIndexConsistentHash(key);
|
|
101
|
+
}
|
|
102
|
+
return this.getIndexModulo(key, shardCount);
|
|
103
|
+
}
|
|
104
|
+
getIndexModulo(key, shardCount) {
|
|
105
|
+
const hash = hashString(key);
|
|
106
|
+
return hash % shardCount;
|
|
107
|
+
}
|
|
108
|
+
getIndexConsistentHash(key) {
|
|
109
|
+
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;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const firstShardId = this.consistentHashRing.get(sortedHashes[0]);
|
|
119
|
+
const match = firstShardId.match(/shard_(\d+)/);
|
|
120
|
+
return match ? parseInt(match[1], 10) - 1 : 0;
|
|
121
|
+
}
|
|
122
|
+
getShardId(key) {
|
|
123
|
+
const index = this.getShardIndex(key);
|
|
124
|
+
return this.shardIds[index] || `shard_${index + 1}`;
|
|
125
|
+
}
|
|
126
|
+
getRandomShardIndex() {
|
|
127
|
+
return Math.floor(Math.random() * this.shardIds.length);
|
|
128
|
+
}
|
|
129
|
+
getRandomShardId() {
|
|
130
|
+
const index = this.getRandomShardIndex();
|
|
131
|
+
return this.shardIds[index];
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
|
|
53
135
|
// src/core/manager.ts
|
|
54
136
|
var ShardManager = class {
|
|
55
137
|
constructor(config) {
|
|
@@ -217,88 +299,6 @@ var ShardManager = class {
|
|
|
217
299
|
}
|
|
218
300
|
};
|
|
219
301
|
|
|
220
|
-
// src/utils/index.ts
|
|
221
|
-
function hashString(str) {
|
|
222
|
-
let hash = 0;
|
|
223
|
-
for (let i = 0; i < str.length; i++) {
|
|
224
|
-
const char = str.charCodeAt(i);
|
|
225
|
-
hash = (hash << 5) - hash + char;
|
|
226
|
-
hash = hash & hash;
|
|
227
|
-
}
|
|
228
|
-
return Math.abs(hash);
|
|
229
|
-
}
|
|
230
|
-
function validateUrl(url) {
|
|
231
|
-
return url.startsWith("postgresql://") || url.startsWith("postgres://");
|
|
232
|
-
}
|
|
233
|
-
function createDefaultLogger() {
|
|
234
|
-
return {
|
|
235
|
-
info: (msg) => console.log(`[PrismaSharding] ${msg}`),
|
|
236
|
-
warn: (msg) => console.warn(`[PrismaSharding] ${msg}`),
|
|
237
|
-
error: (msg) => console.error(`[PrismaSharding] ${msg}`)
|
|
238
|
-
};
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
// src/core/router.ts
|
|
242
|
-
var ShardRouter = class {
|
|
243
|
-
constructor(config) {
|
|
244
|
-
this.consistentHashRing = /* @__PURE__ */ new Map();
|
|
245
|
-
this.virtualNodes = DEFAULTS.CONSISTENT_HASH_VIRTUAL_NODES;
|
|
246
|
-
this.strategy = config.strategy;
|
|
247
|
-
this.shardIds = config.shardIds;
|
|
248
|
-
this.logger = config.logger;
|
|
249
|
-
if (this.strategy === "consistent-hash") {
|
|
250
|
-
this.initializeConsistentHashRing();
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
initializeConsistentHashRing() {
|
|
254
|
-
for (const shardId of this.shardIds) {
|
|
255
|
-
for (let i = 0; i < this.virtualNodes; i++) {
|
|
256
|
-
const hash = hashString(`${shardId}:${i}`);
|
|
257
|
-
this.consistentHashRing.set(hash, shardId);
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
getShardIndex(key) {
|
|
262
|
-
const shardCount = this.shardIds.length;
|
|
263
|
-
if (shardCount === 0) {
|
|
264
|
-
throw new RoutingError("No shards available");
|
|
265
|
-
}
|
|
266
|
-
if (this.strategy === "consistent-hash") {
|
|
267
|
-
return this.getIndexConsistentHash(key);
|
|
268
|
-
}
|
|
269
|
-
return this.getIndexModulo(key, shardCount);
|
|
270
|
-
}
|
|
271
|
-
getIndexModulo(key, shardCount) {
|
|
272
|
-
const hash = hashString(key);
|
|
273
|
-
return hash % shardCount;
|
|
274
|
-
}
|
|
275
|
-
getIndexConsistentHash(key) {
|
|
276
|
-
const hash = hashString(key);
|
|
277
|
-
const sortedHashes = Array.from(this.consistentHashRing.keys()).sort((a, b) => a - b);
|
|
278
|
-
for (const ringHash of sortedHashes) {
|
|
279
|
-
if (hash <= ringHash) {
|
|
280
|
-
const shardId = this.consistentHashRing.get(ringHash);
|
|
281
|
-
const match2 = shardId.match(/shard_(\d+)/);
|
|
282
|
-
return match2 ? parseInt(match2[1], 10) - 1 : 0;
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
const firstShardId = this.consistentHashRing.get(sortedHashes[0]);
|
|
286
|
-
const match = firstShardId.match(/shard_(\d+)/);
|
|
287
|
-
return match ? parseInt(match[1], 10) - 1 : 0;
|
|
288
|
-
}
|
|
289
|
-
getShardId(key) {
|
|
290
|
-
const index = this.getShardIndex(key);
|
|
291
|
-
return this.shardIds[index] || `shard_${index + 1}`;
|
|
292
|
-
}
|
|
293
|
-
getRandomShardIndex() {
|
|
294
|
-
return Math.floor(Math.random() * this.shardIds.length);
|
|
295
|
-
}
|
|
296
|
-
getRandomShardId() {
|
|
297
|
-
const index = this.getRandomShardIndex();
|
|
298
|
-
return this.shardIds[index];
|
|
299
|
-
}
|
|
300
|
-
};
|
|
301
|
-
|
|
302
302
|
// src/core/sharding.ts
|
|
303
303
|
var PrismaSharding = class {
|
|
304
304
|
constructor(config) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "prisma-sharding",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
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",
|
|
@@ -13,9 +13,16 @@
|
|
|
13
13
|
"sharding",
|
|
14
14
|
"database",
|
|
15
15
|
"postgresql",
|
|
16
|
+
"db-sharding",
|
|
16
17
|
"horizontal-scaling",
|
|
17
|
-
"connection-pool"
|
|
18
|
+
"connection-pool",
|
|
19
|
+
"cli"
|
|
18
20
|
],
|
|
21
|
+
"bin": {
|
|
22
|
+
"prisma-sharding-migrate": "./dist/cli/migrate.js",
|
|
23
|
+
"prisma-sharding-studio": "./dist/cli/studio.js",
|
|
24
|
+
"prisma-sharding-test": "./dist/cli/test.js"
|
|
25
|
+
},
|
|
19
26
|
"files": [
|
|
20
27
|
"dist"
|
|
21
28
|
],
|
|
@@ -27,7 +34,9 @@
|
|
|
27
34
|
}
|
|
28
35
|
},
|
|
29
36
|
"scripts": {
|
|
30
|
-
"build": "tsup src/index.ts --format cjs,esm --dts --clean",
|
|
37
|
+
"build": "tsup src/index.ts --format cjs,esm --dts --clean && tsup src/cli/migrate.ts src/cli/studio.ts src/cli/test.ts --format cjs --outDir dist/cli --clean",
|
|
38
|
+
"build:lib": "tsup src/index.ts --format cjs,esm --dts --clean",
|
|
39
|
+
"build:cli": "tsup src/cli/migrate.ts src/cli/studio.ts src/cli/test.ts --format cjs --outDir dist/cli",
|
|
31
40
|
"dev": "tsup src/index.ts --format cjs,esm --dts --watch",
|
|
32
41
|
"lint": "eslint src --ext .ts",
|
|
33
42
|
"typecheck": "tsc --noEmit",
|
|
@@ -40,6 +49,9 @@
|
|
|
40
49
|
"peerDependencies": {
|
|
41
50
|
"@prisma/client": ">=5.0.0"
|
|
42
51
|
},
|
|
52
|
+
"dependencies": {
|
|
53
|
+
"dotenv": "^16.3.1"
|
|
54
|
+
},
|
|
43
55
|
"devDependencies": {
|
|
44
56
|
"@types/node": "^20.10.0",
|
|
45
57
|
"@typescript-eslint/eslint-plugin": "^6.13.0",
|