@earth-app/collegedb 1.0.2 → 1.0.4
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 +183 -33
- package/dist/index.js +7 -7
- package/dist/index.js.map +5 -5
- package/dist/kvmap.d.ts +80 -26
- package/dist/kvmap.d.ts.map +1 -1
- package/dist/migrations.d.ts +40 -2
- package/dist/migrations.d.ts.map +1 -1
- package/dist/router.d.ts +39 -0
- package/dist/router.d.ts.map +1 -1
- package/dist/types.d.ts +24 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -189,6 +189,96 @@ This approach provides:
|
|
|
189
189
|
- **Optimal read performance**: Queries use `hash` strategy for consistent, high-performance routing
|
|
190
190
|
- **Flexibility**: Each operation type can use the most appropriate routing strategy
|
|
191
191
|
|
|
192
|
+
## Multi-Key Shard Mappings
|
|
193
|
+
|
|
194
|
+
CollegeDB supports **multiple lookup keys** for the same record, allowing you to query by username, email, ID, or any unique identifier. Keys are automatically hashed with SHA-256 for security and privacy.
|
|
195
|
+
|
|
196
|
+
```typescript
|
|
197
|
+
import { collegedb, first, run, KVShardMapper } from 'collegedb';
|
|
198
|
+
|
|
199
|
+
collegedb(
|
|
200
|
+
{
|
|
201
|
+
kv: env.KV,
|
|
202
|
+
shards: { 'db-east': env.DB_EAST, 'db-west': env.DB_WEST },
|
|
203
|
+
hashShardMappings: true, // Default: enabled for security
|
|
204
|
+
strategy: 'hash'
|
|
205
|
+
},
|
|
206
|
+
async () => {
|
|
207
|
+
// Create a user with multiple lookup keys
|
|
208
|
+
const mapper = new KVShardMapper(env.KV, { hashShardMappings: true });
|
|
209
|
+
|
|
210
|
+
await mapper.setShardMapping('user-123', 'db-east', ['username:john_doe', 'email:john@example.com', 'id:123']);
|
|
211
|
+
|
|
212
|
+
// Now you can query by ANY of these keys
|
|
213
|
+
const byId = await first('user-123', 'SELECT * FROM users WHERE id = ?', ['user-123']);
|
|
214
|
+
const byUsername = await first('username:john_doe', 'SELECT * FROM users WHERE username = ?', ['john_doe']);
|
|
215
|
+
const byEmail = await first('email:john@example.com', 'SELECT * FROM users WHERE email = ?', ['john@example.com']);
|
|
216
|
+
|
|
217
|
+
// All queries route to the same shard (db-east)
|
|
218
|
+
console.log('All queries find the same user:', byId?.name);
|
|
219
|
+
}
|
|
220
|
+
);
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Adding Lookup Keys to Existing Mappings
|
|
224
|
+
|
|
225
|
+
s
|
|
226
|
+
|
|
227
|
+
```typescript
|
|
228
|
+
const mapper = new KVShardMapper(env.KV);
|
|
229
|
+
|
|
230
|
+
// User initially created with just ID
|
|
231
|
+
await mapper.setShardMapping('user-456', 'db-west');
|
|
232
|
+
|
|
233
|
+
// Later, add additional lookup methods
|
|
234
|
+
await mapper.addLookupKeys('user-456', ['email:jane@example.com', 'username:jane']);
|
|
235
|
+
|
|
236
|
+
// Now works with any key
|
|
237
|
+
const user = await first('email:jane@example.com', 'SELECT * FROM users WHERE email = ?', ['jane@example.com']);
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### Security and Privacy
|
|
241
|
+
|
|
242
|
+
**SHA-256 Hashing (Enabled by Default)**: Sensitive data like emails are hashed before being stored as KV keys, protecting user privacy:
|
|
243
|
+
|
|
244
|
+
```typescript
|
|
245
|
+
// With hashShardMappings: true (default)
|
|
246
|
+
// KV stores: "shard:a1b2c3d4..." instead of "shard:email:user@example.com"
|
|
247
|
+
|
|
248
|
+
const config = {
|
|
249
|
+
kv: env.KV,
|
|
250
|
+
shards: {
|
|
251
|
+
/* ... */
|
|
252
|
+
},
|
|
253
|
+
hashShardMappings: true, // Hashes keys with SHA-256
|
|
254
|
+
strategy: 'hash'
|
|
255
|
+
};
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
**⚠️ Performance Trade-off**: When hashing is enabled, operations like `getKeysForShard()` cannot return original key names, only hashed versions. For full key recovery, disable hashing:
|
|
259
|
+
|
|
260
|
+
```typescript
|
|
261
|
+
const config = {
|
|
262
|
+
hashShardMappings: false // Disables hashing - keys stored in plain text
|
|
263
|
+
};
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
### Multi-Key Management
|
|
267
|
+
|
|
268
|
+
```typescript
|
|
269
|
+
const mapper = new KVShardMapper(env.KV);
|
|
270
|
+
|
|
271
|
+
// Get all lookup keys for a mapping
|
|
272
|
+
const allKeys = await mapper.getAllLookupKeys('email:user@example.com');
|
|
273
|
+
console.log(allKeys); // ['user-123', 'username:john', 'email:user@example.com']
|
|
274
|
+
|
|
275
|
+
// Update shard assignment (updates all keys)
|
|
276
|
+
await mapper.updateShardMapping('username:john', 'db-central');
|
|
277
|
+
|
|
278
|
+
// Delete mapping (removes all associated keys)
|
|
279
|
+
await mapper.deleteShardMapping('user-123');
|
|
280
|
+
```
|
|
281
|
+
|
|
192
282
|
## Drop-in Replacement for Existing Databases
|
|
193
283
|
|
|
194
284
|
CollegeDB supports **seamless, automatic integration** with existing D1 databases that already contain data. Simply add your existing databases as shards in the configuration. CollegeDB will automatically detect existing data and create the necessary shard mappings **without requiring any manual migration steps**.
|
|
@@ -441,35 +531,38 @@ for (const [table, pkColumn] of Object.entries(customIntegration)) {
|
|
|
441
531
|
|
|
442
532
|
## 📚 API Reference
|
|
443
533
|
|
|
444
|
-
| Function
|
|
445
|
-
|
|
|
446
|
-
| `collegedb(config, callback)`
|
|
447
|
-
| `initialize(config)`
|
|
448
|
-
| `createSchema(d1)`
|
|
449
|
-
| `prepare(key, sql)`
|
|
450
|
-
| `run(key, sql, bindings)`
|
|
451
|
-
| `first(key, sql, bindings)`
|
|
452
|
-
| `all(key, sql, bindings)`
|
|
453
|
-
| `runShard(shard, sql, bindings)`
|
|
454
|
-
| `allShard(shard, sql, bindings)`
|
|
455
|
-
| `firstShard(shard, sql, bindings)`
|
|
456
|
-
| `
|
|
457
|
-
| `
|
|
458
|
-
| `
|
|
459
|
-
| `
|
|
534
|
+
| Function | Description | Parameters |
|
|
535
|
+
| ------------------------------------------ | ---------------------------------------------------------------- | -------------------------- |
|
|
536
|
+
| `collegedb(config, callback)` | Initialize CollegeDB, then run a callback | `CollegeDBConfig, () => T` |
|
|
537
|
+
| `initialize(config)` | Initialize CollegeDB with configuration | `CollegeDBConfig` |
|
|
538
|
+
| `createSchema(d1)` | Create database schema on a D1 instance | `D1Database` |
|
|
539
|
+
| `prepare(key, sql)` | Prepare a SQL statement for execution | `string, string` |
|
|
540
|
+
| `run(key, sql, bindings)` | Execute a SQL query with primary key routing | `string, string, any[]` |
|
|
541
|
+
| `first(key, sql, bindings)` | Execute a SQL query and return first result | `string, string, any[]` |
|
|
542
|
+
| `all(key, sql, bindings)` | Execute a SQL query and return all results | `string, string, any[]` |
|
|
543
|
+
| `runShard(shard, sql, bindings)` | Execute a query directly on a specific shard | `string, string, any[]` |
|
|
544
|
+
| `allShard(shard, sql, bindings)` | Execute a query on specific shard, return all results | `string, string, any[]` |
|
|
545
|
+
| `firstShard(shard, sql, bindings)` | Execute a query on specific shard, return first result | `string, string, any[]` |
|
|
546
|
+
| `runAllShards(sql, bindings, batchSize)` | Execute query on all shards | `string, any[], number` |
|
|
547
|
+
| `allAllShards(sql, bindings, batchSize)` | Execute query on all shards, return all results from all shards | `string, any[], number` |
|
|
548
|
+
| `firstAllShards(sql, bindings, batchSize)` | Execute query on all shards, return first result from all shards | `string, any[], number` |
|
|
549
|
+
| `reassignShard(key, newShard)` | Move primary key to different shard | `string, string` |
|
|
550
|
+
| `listKnownShards()` | Get list of available shards | `void` |
|
|
551
|
+
| `getShardStats()` | Get statistics for all shards | `void` |
|
|
552
|
+
| `flush()` | Clear all shard mappings (development only) | `void` |
|
|
460
553
|
|
|
461
554
|
### Drop-in Replacement Functions
|
|
462
555
|
|
|
463
|
-
| Function | Description
|
|
464
|
-
| ----------------------------------------- |
|
|
465
|
-
| `autoDetectAndMigrate(d1, shard, config)` |
|
|
466
|
-
| `checkMigrationNeeded(d1, shard, config)` |
|
|
467
|
-
| `validateTableForSharding(d1, table)` | Check if table is suitable for sharding
|
|
468
|
-
| `discoverExistingPrimaryKeys(d1, table)` | Find all primary keys in existing table
|
|
469
|
-
| `integrateExistingDatabase(d1, shard)` | Complete drop-in integration of existing DB
|
|
470
|
-
| `createMappingsForExistingKeys(keys)` | Create shard mappings for existing keys
|
|
471
|
-
| `listTables(d1)` | Get list of tables in database
|
|
472
|
-
| `clearMigrationCache()` | Clear automatic migration cache
|
|
556
|
+
| Function | Description | Parameters |
|
|
557
|
+
| ----------------------------------------- | ---------------------------------------------- | ------------------------------ |
|
|
558
|
+
| `autoDetectAndMigrate(d1, shard, config)` | Automatically detect and migrate existing data | `D1Database, string, config` |
|
|
559
|
+
| `checkMigrationNeeded(d1, shard, config)` | Check if database needs migration | `D1Database, string, config` |
|
|
560
|
+
| `validateTableForSharding(d1, table)` | Check if table is suitable for sharding | `D1Database, string` |
|
|
561
|
+
| `discoverExistingPrimaryKeys(d1, table)` | Find all primary keys in existing table | `D1Database, string` |
|
|
562
|
+
| `integrateExistingDatabase(d1, shard)` | Complete drop-in integration of existing DB | `D1Database, string, mapper` |
|
|
563
|
+
| `createMappingsForExistingKeys(keys)` | Create shard mappings for existing keys | `string[], string[], strategy` |
|
|
564
|
+
| `listTables(d1)` | Get list of tables in database | `D1Database` |
|
|
565
|
+
| `clearMigrationCache()` | Clear automatic migration cache | `void` |
|
|
473
566
|
|
|
474
567
|
### Error Handling
|
|
475
568
|
|
|
@@ -557,10 +650,13 @@ interface CollegeDBConfig {
|
|
|
557
650
|
strategy?: ShardingStrategy | MixedShardingStrategy;
|
|
558
651
|
targetRegion?: D1Region;
|
|
559
652
|
shardLocations?: Record<string, ShardLocation>;
|
|
560
|
-
disableAutoMigration?: boolean;
|
|
653
|
+
disableAutoMigration?: boolean; // Default: false
|
|
654
|
+
hashShardMappings?: boolean; // Default: true
|
|
561
655
|
}
|
|
562
656
|
```
|
|
563
657
|
|
|
658
|
+
When `hashShardMappings` is enabled (default), original keys cannot be recovered during shard operations like `getKeysForShard()`. This is intentional for privacy but means you'll get fewer results from such operations. For full key recovery, set `hashShardMappings: false`, but be aware this may expose sensitive data in KV keys.
|
|
659
|
+
|
|
564
660
|
#### Strategy Types
|
|
565
661
|
|
|
566
662
|
```typescript
|
|
@@ -1176,7 +1272,7 @@ _SELECT, VALUES, TABLE, PRAGMA, ..._
|
|
|
1176
1272
|
| CollegeDB (100 shards) | ~60-90ms | 100x parallel capacity | ~75-80x |
|
|
1177
1273
|
| CollegeDB (1000 shards) | ~65-95ms | 1000x parallel capacity | ~650-700x |
|
|
1178
1274
|
|
|
1179
|
-
\*Includes KV lookup overhead (~5-15ms)
|
|
1275
|
+
\*Includes KV lookup overhead (~5-15ms) and SHA-256 hashing overhead (~1-3ms when `hashShardMappings: true`)
|
|
1180
1276
|
|
|
1181
1277
|
#### Write Performance
|
|
1182
1278
|
|
|
@@ -1189,14 +1285,14 @@ _INSERT, UPDATE, DELETE, ..._
|
|
|
1189
1285
|
| CollegeDB (100 shards) | ~95-145ms | ~4,200 writes/sec | ~84x |
|
|
1190
1286
|
| CollegeDB (1000 shards) | ~105-160ms | ~35,000 writes/sec | ~700x |
|
|
1191
1287
|
|
|
1192
|
-
\*Includes KV mapping creation/update overhead (~10-25ms)
|
|
1288
|
+
\*Includes KV mapping creation/update overhead (~10-25ms) and SHA-256 hashing overhead (~1-3ms when `hashShardMappings: true`)
|
|
1193
1289
|
|
|
1194
1290
|
### Strategy-Specific Performance
|
|
1195
1291
|
|
|
1196
1292
|
#### Hash Strategy
|
|
1197
1293
|
|
|
1198
1294
|
- **Best for**: Consistent performance, even data distribution
|
|
1199
|
-
- **Latency**: Lowest overhead (no coordinator calls)
|
|
1295
|
+
- **Latency**: Lowest overhead (no coordinator calls, ~1-3ms SHA-256 hashing when enabled)
|
|
1200
1296
|
- **Throughput**: Optimal for high-volume scenarios
|
|
1201
1297
|
|
|
1202
1298
|
| Shards | Avg Latency | Distribution Quality | Coordinator Dependency |
|
|
@@ -1323,7 +1419,7 @@ _INSERT, UPDATE, DELETE, ..._
|
|
|
1323
1419
|
// Recommended: Hash reads + Location writes
|
|
1324
1420
|
{
|
|
1325
1421
|
strategy: { read: 'hash', write: 'location' },
|
|
1326
|
-
targetRegion:
|
|
1422
|
+
targetRegion: getClosestRegionFromIP(request), // Dynamic region targeting
|
|
1327
1423
|
shardLocations: {
|
|
1328
1424
|
'db-americas': { region: 'wnam', priority: 2 },
|
|
1329
1425
|
'db-europe': { region: 'weur', priority: 2 },
|
|
@@ -1430,6 +1526,58 @@ _INSERT, UPDATE, DELETE, ..._
|
|
|
1430
1526
|
| **Balanced** | 50/50 | `{read: 'hash', write: 'hash'}` | Consistent performance |
|
|
1431
1527
|
| **Analytics** | 95% reads | `{read: 'location', write: 'round-robin'}` | Regional + perfect distribution |
|
|
1432
1528
|
|
|
1529
|
+
### SHA-256 Hashing Performance Impact
|
|
1530
|
+
|
|
1531
|
+
CollegeDB uses SHA-256 hashing by default (`hashShardMappings: true`) to protect sensitive data in KV keys. This adds a small but measurable performance overhead:
|
|
1532
|
+
|
|
1533
|
+
#### Hashing Performance Characteristics
|
|
1534
|
+
|
|
1535
|
+
| Operation Type | SHA-256 Overhead | Total Latency Impact | Security Benefit |
|
|
1536
|
+
| ------------------ | ---------------- | -------------------- | ---------------------------- |
|
|
1537
|
+
| **Query (Read)** | ~1-2ms | 2-4% increase | Keys hashed in KV storage |
|
|
1538
|
+
| **Insert (Write)** | ~2-3ms | 2-3% increase | Multi-key mappings protected |
|
|
1539
|
+
| **Update Mapping** | ~1-3ms | 1-2% increase | Existing keys remain secure |
|
|
1540
|
+
|
|
1541
|
+
#### Performance by Key Length
|
|
1542
|
+
|
|
1543
|
+
| Key Type | Example | Hash Time | Recommendation |
|
|
1544
|
+
| ------------------------ | ------------------------------ | ------------ | ----------------------- |
|
|
1545
|
+
| **Short keys** | `user-123` | ~0.5-1ms | Minimal impact |
|
|
1546
|
+
| **Medium keys** | `email:user@example.com` | ~1-2ms | Good balance |
|
|
1547
|
+
| **Long keys** | `session:very-long-token-here` | ~2-3ms | Consider key shortening |
|
|
1548
|
+
| **Multi-key operations** | 3+ lookup keys | ~3-5ms total | Benefits outweigh cost |
|
|
1549
|
+
|
|
1550
|
+
#### Hashing vs No-Hashing Trade-offs
|
|
1551
|
+
|
|
1552
|
+
```typescript
|
|
1553
|
+
// With hashing (default - recommended for production)
|
|
1554
|
+
const secureConfig = {
|
|
1555
|
+
hashShardMappings: true // Default
|
|
1556
|
+
// + Privacy: Sensitive data not visible in KV
|
|
1557
|
+
// + Security: Keys cannot be enumerated
|
|
1558
|
+
// - Performance: +1-3ms per operation
|
|
1559
|
+
// - Debugging: Original keys not recoverable
|
|
1560
|
+
};
|
|
1561
|
+
|
|
1562
|
+
// Without hashing (development/debugging only)
|
|
1563
|
+
const developmentConfig = {
|
|
1564
|
+
hashShardMappings: false
|
|
1565
|
+
// + Performance: No hashing overhead
|
|
1566
|
+
// + Debugging: Original keys visible in KV
|
|
1567
|
+
// - Privacy: Sensitive data exposed in KV keys
|
|
1568
|
+
// - Security: Keys can be enumerated
|
|
1569
|
+
};
|
|
1570
|
+
```
|
|
1571
|
+
|
|
1572
|
+
#### Optimization Recommendations
|
|
1573
|
+
|
|
1574
|
+
1. **Keep keys reasonably short** - Hash time scales with key length
|
|
1575
|
+
2. **Use hashing in production** - Security benefits outweigh minimal performance cost
|
|
1576
|
+
3. **Disable hashing for development** - When debugging shard distribution
|
|
1577
|
+
4. **Monitor hash performance** - Track operation latencies in high-volume scenarios
|
|
1578
|
+
|
|
1579
|
+
**Bottom Line**: SHA-256 hashing adds 1-3ms overhead but provides essential privacy and security benefits. The performance impact is minimal compared to network latency and D1 query time.
|
|
1580
|
+
|
|
1433
1581
|
### Real-World Scaling Benefits
|
|
1434
1582
|
|
|
1435
1583
|
#### Database Size Limits
|
|
@@ -1619,7 +1767,7 @@ const stats = await statsResponse.json();
|
|
|
1619
1767
|
"lastUpdated": 1672531200000
|
|
1620
1768
|
},
|
|
1621
1769
|
{
|
|
1622
|
-
"binding": "db-west",
|
|
1770
|
+
"binding": "db-west",
|
|
1623
1771
|
"count": 1458,
|
|
1624
1772
|
"lastUpdated": 1672531205000
|
|
1625
1773
|
}
|
|
@@ -1742,7 +1890,9 @@ async function monitorShardHealth(env: Env) {
|
|
|
1742
1890
|
}
|
|
1743
1891
|
```
|
|
1744
1892
|
|
|
1745
|
-
#### Error Handling
|
|
1893
|
+
#### Error Handling with ShardCoordinator
|
|
1894
|
+
|
|
1895
|
+
When using the ShardCoordinator, ensure you handle potential errors gracefully:
|
|
1746
1896
|
|
|
1747
1897
|
```typescript
|
|
1748
1898
|
try {
|
package/dist/index.js
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
var
|
|
1
|
+
var jJ=Object.defineProperty;var i=(J,Z)=>{for(var $ in Z)jJ(J,$,{get:Z[$],enumerable:!0,configurable:!0,set:(V)=>Z[$]=()=>V})};var m=(J,Z)=>()=>(J&&(Z=J(J=0)),Z);var G;var k=m(()=>{G=class G extends Error{code;constructor(J,Z){super(J);if(this.name="CollegeDBError",this.code=Z,Error.captureStackTrace)Error.captureStackTrace(this,G)}}});var y={};i(y,{KVShardMapper:()=>E});class E{kv;hashKeys;constructor(J,Z={}){this.kv=J,this.hashKeys=Z.hashShardMappings??!0}async hashKey(J){if(!this.hashKeys)return J;let $=new TextEncoder().encode(J),V=await crypto.subtle.digest("SHA-256",$),Q=new Uint8Array(V);return Array.from(Q).map((O)=>O.toString(16).padStart(2,"0")).join("")}async getShardMapping(J){let Z=await this.hashKey(J),$=`${w}${Z}`,V=await this.kv.get($,"json");if(V)return V;let Q=await this.kv.get(`${B}${Z}`,"json");if(Q)return{shard:Q.shard,createdAt:Q.createdAt,updatedAt:Q.updatedAt,originalKey:this.hashKeys?void 0:J};return null}async setShardMapping(J,Z,$=[]){let V=[J,...$],Q=Date.now();if(V.length===1){let W=await this.hashKey(J),O=`${w}${W}`,z={shard:Z,createdAt:Q,updatedAt:Q,originalKey:this.hashKeys?void 0:J};await this.kv.put(O,JSON.stringify(z))}else{let W=await this.hashKey(J),O=`${B}${W}`,z={shard:Z,createdAt:Q,updatedAt:Q,keys:this.hashKeys?[]:V};await this.kv.put(O,JSON.stringify(z));let U=V.map(async(j)=>{let Y=await this.hashKey(j),x=`${w}${Y}`,F={shard:Z,createdAt:Q,updatedAt:Q,originalKey:this.hashKeys?void 0:j};return this.kv.put(x,JSON.stringify(F))});await Promise.all(U)}}async updateShardMapping(J,Z){let $=await this.getShardMapping(J);if(!$)throw new G(`No existing mapping found for primary key: ${J}`,"MAPPING_NOT_FOUND");let V=await this.hashKey(J),Q=`${w}${V}`,W=`${B}${V}`,O=await this.kv.get(W,"json");if(O){let z={...O,shard:Z,updatedAt:Date.now()};await this.kv.put(W,JSON.stringify(z));let U=O.keys.map(async(j)=>{let Y=await this.hashKey(j),x=`${w}${Y}`,F={...$,shard:Z,updatedAt:Date.now()};return this.kv.put(x,JSON.stringify(F))});await Promise.all(U)}else{let z={...$,shard:Z,updatedAt:Date.now()};await this.kv.put(Q,JSON.stringify(z))}}async deleteShardMapping(J){let Z=await this.hashKey(J),$=`${w}${Z}`,V=`${B}${Z}`,Q=await this.kv.get(V,"json");if(Q){await this.kv.delete(V);let W=Q.keys.map(async(O)=>{let z=await this.hashKey(O),U=`${w}${z}`;return this.kv.delete(U)});await Promise.all(W)}else await this.kv.delete($)}async getKnownShards(){return await this.kv.get(s,"json")||[]}async setKnownShards(J){if(!J||J.length===0)return;await this.kv.put(s,JSON.stringify(J))}async addKnownShard(J){if(!J)return;let Z=await this.getKnownShards();if(!Z.includes(J))Z.push(J),await this.setKnownShards(Z)}async getKeysForShard(J){let Z=[],$=await this.kv.list({prefix:w});for(let Q of $.keys){let W=await this.kv.get(Q.name,"json");if(W?.shard===J){let O=Q.name.replace(w,"");if(W.originalKey)Z.push(W.originalKey);else if(!this.hashKeys)Z.push(O)}}let V=await this.kv.list({prefix:B});for(let Q of V.keys){let W=await this.kv.get(Q.name,"json");if(W?.shard===J)Z.push(...W.keys)}return[...new Set(Z)]}async getShardKeyCounts(){let J={},Z=await this.kv.list({prefix:w});for(let V of Z.keys){let Q=await this.kv.get(V.name,"json");if(Q)J[Q.shard]=(J[Q.shard]||0)+1}let $=await this.kv.list({prefix:B});for(let V of $.keys){let Q=await this.kv.get(V.name,"json");if(Q)J[Q.shard]=(J[Q.shard]||0)+Q.keys.length}return J}async clearAllMappings(){let Z=(await this.kv.list({prefix:w})).keys.map((Q)=>this.kv.delete(Q.name)),V=(await this.kv.list({prefix:B})).keys.map((Q)=>this.kv.delete(Q.name));await Promise.all([...Z,...V])}async addLookupKeys(J,Z){let $=await this.getShardMapping(J);if(!$)throw new G(`No existing mapping found for primary key: ${J}`,"MAPPING_NOT_FOUND");let V=await this.hashKey(J),Q=`${B}${V}`,W=await this.kv.get(Q,"json"),O=[J,...Z],z=Date.now();if(!W)W={shard:$.shard,createdAt:$.createdAt,updatedAt:z,keys:this.hashKeys?[]:O};else W={...W,updatedAt:z,keys:this.hashKeys?[]:[...new Set([...W.keys,...O])]};await this.kv.put(Q,JSON.stringify(W));let U=Z.map(async(j)=>{let Y=await this.hashKey(j),x=`${w}${Y}`,F={shard:$.shard,createdAt:$.createdAt,updatedAt:z,originalKey:this.hashKeys?void 0:j};return this.kv.put(x,JSON.stringify(F))});await Promise.all(U)}async getAllLookupKeys(J){let Z=await this.hashKey(J),$=`${B}${Z}`,V=await this.kv.get($,"json");if(V)return V.keys;let Q=await this.getShardMapping(J);if(Q)return Q.originalKey?[Q.originalKey]:[J];throw new G(`No mapping found for key: ${J}`,"MAPPING_NOT_FOUND")}}var w="shard:",B="multikey:",s="known_shards";var f=m(()=>{k()});var b={};i(b,{validateTableForSharding:()=>g,schemaExists:()=>d,migrateRecord:()=>e,listTables:()=>C,integrateExistingDatabase:()=>ZJ,dropSchema:()=>a,discoverExistingRecordsWithColumns:()=>n,discoverExistingPrimaryKeys:()=>c,createSchemaAcrossShards:()=>r,createSchema:()=>l,createMappingsForExistingKeys:()=>JJ,clearShardMigrationCache:()=>WJ,clearMigrationCache:()=>VJ,checkMigrationNeeded:()=>QJ,autoDetectAndMigrate:()=>$J});async function l(J,Z){let $=Z.split(";").map((V)=>V.trim()).filter((V)=>V.length>0&&!V.startsWith("--"));for(let V of $)try{await J.prepare(V).run()}catch(Q){throw console.error("Failed to execute schema statement:",V,Q),new G(`Schema migration failed: ${Q}`,"SCHEMA_MIGRATION_FAILED")}}async function r(J,Z){let $=Object.entries(J).map(([V,Q])=>{return l(Q,Z).catch((W)=>{throw new G(`Failed to create schema on shard ${V}: ${W.message}`,"SCHEMA_CREATION_FAILED")})});await Promise.all($)}async function d(J,Z){try{return await J.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name=?").bind(Z).first()!==null}catch{return!1}}async function a(J,...Z){for(let $ of Z)try{await J.prepare(`DROP TABLE IF EXISTS ${$}`).run()}catch(V){console.error(`Failed to drop table ${$}:`,V)}}async function C(J){try{return(await J.prepare("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name").all()).results.map(($)=>$.name)}catch{return[]}}async function e(J,Z,$,V){let Q=await J.prepare(`SELECT * FROM ${V} WHERE id = ?`).bind($).first();if(!Q)throw new G(`Record with primary key ${$} not found in source database`,"RECORD_NOT_FOUND");if(!await d(Z,V))await l(Z,V);let W=Object.keys(Q),O=W.map(()=>"?").join(", "),z=W.map((j)=>Q[j]),U=`INSERT OR REPLACE INTO ${V} (${W.join(", ")}) VALUES (${O})`;await Z.prepare(U).bind(...z).run(),await J.prepare(`DELETE FROM ${V} WHERE id = ?`).bind($).run()}async function c(J,Z,$="id"){try{return(await J.prepare(`SELECT ${$} FROM ${Z}`).all()).results.map((Q)=>String(Q[$]))}catch(V){throw new G(`Failed to discover primary keys in table ${Z}: ${V}`,"DISCOVERY_FAILED")}}async function n(J,Z,$="id"){try{let Q=(await J.prepare(`PRAGMA table_info(${Z})`).all()).results.map((U)=>U.name),W=[$];if(Q.includes("username"))W.push("username");if(Q.includes("email"))W.push("email");if(Q.includes("name"))W.push("name");let O=`SELECT ${W.join(", ")} FROM ${Z}`;return(await J.prepare(O).all()).results}catch(V){throw new G(`Failed to discover records with columns in table ${Z}: ${V}`,"DISCOVERY_FAILED")}}async function JJ(J,Z,$,V){let Q=Z.length;for(let W=0;W<J.length;W++){let O=J[W],z;switch($){case"hash":let U=0;for(let Y=0;Y<O.length;Y++){let x=O.charCodeAt(Y);U=(U<<5)-U+x,U=U&U}let j=Math.abs(U)%Q;z=Z[j];break;case"random":z=Z[Math.floor(Math.random()*Q)];break;default:z=Z[W%Q];break}await V.setShardMapping(O,z)}}async function g(J,Z,$){let V=[],Q=0;try{if(!await J.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name=?").bind(Z).first())return V.push(`Table '${Z}' does not exist`),{isValid:!1,tableName:Z,primaryKeyColumn:$,recordCount:0,issues:V};if(!(await J.prepare(`PRAGMA table_info(${Z})`).all()).results.some((j)=>j.name===$&&j.pk===1))V.push(`Primary key column '${$}' not found or not set as primary key`);if(Q=(await J.prepare(`SELECT COUNT(*) as count FROM ${Z}`).first())?.count||0,Q===0)V.push(`Table '${Z}' is empty`)}catch(W){V.push(`Database validation error: ${W}`)}return{isValid:V.length===0,tableName:Z,primaryKeyColumn:$,recordCount:Q,issues:V}}async function ZJ(J,Z,$,V={}){let{tables:Q,primaryKeyColumn:W="id",strategy:O="hash",addShardMappingsTable:z=!0,dryRun:U=!1,migrateOtherColumns:j=!1}=V,Y=[],x=0,F=0,H=0;try{let S=(Q||await C(J)).filter((X)=>X!=="shard_mappings");for(let X of S)try{let I=await g(J,X,W);if(!I.isValid){Y.push(`Table ${X}: ${I.issues.join(", ")}`);continue}if(j){let D=await n(J,X,W);if(D.length===0){Y.push(`Table ${X} has no records to process`);continue}if(!U)for(let L of D){let R=String(L[W]),P=[];if(L.username&&typeof L.username==="string")P.push(`username:${L.username}`);if(L.email&&typeof L.email==="string")P.push(`email:${L.email}`);if(L.name&&typeof L.name==="string")P.push(`name:${L.name}`);await $.setShardMapping(R,Z,P),H++}F+=D.length}else{let D=await c(J,X,W);if(D.length===0){Y.push(`Table ${X} has no records to process`);continue}if(!U)for(let L of D)await $.setShardMapping(L,Z),H++;F+=D.length}x++}catch(I){Y.push(`Failed to process table ${X}: ${I}`)}if(z&&!U){if(!(await C(J)).includes("shard_mappings"))await J.prepare(`
|
|
2
2
|
CREATE TABLE IF NOT EXISTS shard_mappings (
|
|
3
3
|
primary_key TEXT PRIMARY KEY,
|
|
4
4
|
shard_name TEXT NOT NULL,
|
|
5
5
|
created_at INTEGER NOT NULL,
|
|
6
6
|
updated_at INTEGER NOT NULL
|
|
7
|
-
);`.trim()).run()}if(!
|
|
8
|
-
SELECT ${
|
|
9
|
-
ORDER BY ${
|
|
10
|
-
LIMIT ?`.trim()).bind(
|
|
7
|
+
);`.trim()).run()}if(!U)await $.addKnownShard(Z)}catch(A){Y.push(`Integration failed: ${A}`)}return{success:Y.length===0||Y.length>0&&x>0,shardName:Z,tablesProcessed:x,totalRecords:F,mappingsCreated:H,issues:Y}}async function $J(J,Z,$,V={}){let{primaryKeyColumn:Q="id",tablesToCheck:W,skipCache:O=!1,maxRecordsToCheck:z=1000,migrateOtherColumns:U=!1}=V,j=`${Z}_migration_check`;if(!O&&q.has(j))return{migrationNeeded:!1,migrationPerformed:!1,recordsMigrated:0,tablesProcessed:0,issues:[]};let Y=[],x=0,F=0,H=!1,A=!1;try{let{KVShardMapper:S}=await Promise.resolve().then(() => (f(),y)),X=new S($.kv,{hashShardMappings:$.hashShardMappings}),I=await C(J),D=W||I.filter((L)=>L!=="shard_mappings"&&!L.startsWith("sqlite_")&&L!=="sqlite_sequence");if(D.length===0)return q.set(j,!0),{migrationNeeded:!1,migrationPerformed:!1,recordsMigrated:0,tablesProcessed:0,issues:[]};for(let L of D)try{let R=await g(J,L,Q);if(!R.isValid||R.recordCount===0)continue;let P=Math.min(z,R.recordCount),YJ=await J.prepare(`
|
|
8
|
+
SELECT ${Q} FROM ${L}
|
|
9
|
+
ORDER BY ${Q}
|
|
10
|
+
LIMIT ?`.trim()).bind(P).all(),t=0,zJ=YJ.results.slice(0,10);for(let N of zJ){let _=String(N[Q]);if(!await X.getShardMapping(_))t++,H=!0}if(t>0){if(console.log(`Auto-migrating table ${L} in shard ${Z} (${R.recordCount} records)`),U){let N=await n(J,L,Q),_=0;for(let T of N){let p=String(T[Q]);if(!await X.getShardMapping(p)){let K=[];if(T.username&&typeof T.username==="string")K.push(`username:${T.username}`);if(T.email&&typeof T.email==="string")K.push(`email:${T.email}`);if(T.name&&typeof T.name==="string")K.push(`name:${T.name}`);await X.setShardMapping(p,Z,K),_++}}x+=_}else{let N=await c(J,L,Q),_=0;for(let T of N)if(!await X.getShardMapping(T))await X.setShardMapping(T,Z),_++;x+=_}F++,A=!0,console.log(`Auto-migrated ${x} records from table ${L}`)}}catch(R){Y.push(`Auto-migration failed for table ${L}: ${R}`)}if(A){if(await X.addKnownShard(Z),!I.includes("shard_mappings"))await J.prepare(`CREATE TABLE IF NOT EXISTS shard_mappings (
|
|
11
11
|
primary_key TEXT PRIMARY KEY,
|
|
12
12
|
shard_name TEXT NOT NULL,
|
|
13
13
|
created_at INTEGER NOT NULL,
|
|
14
14
|
updated_at INTEGER NOT NULL
|
|
15
15
|
);
|
|
16
|
-
`).run()}if(E.set(W,!0),F)console.log(`Auto-migration completed for shard ${Q}: ${L} records from ${X} tables`)}catch(T){O.push(`Auto-migration error: ${T}`)}return{migrationNeeded:x,migrationPerformed:F,recordsMigrated:L,tablesProcessed:X,issues:O}}async function r(J,Q,U){let Z=`${Q}_migration_check`;if(E.has(Z))return!1;try{let $=await P(J);if($.includes("shard_mappings"))return E.set(Z,!0),!1;let{KVShardMapper:Y}=await Promise.resolve().then(() => (v(),g)),V=new Y(U.kv),W=$.filter((O)=>O!=="shard_mappings"&&!O.startsWith("sqlite_")&&O!=="sqlite_sequence");for(let O of W.slice(0,3))try{if(((await J.prepare(`SELECT COUNT(*) as count FROM ${O} LIMIT 1`).first())?.count||0)>0){let x=await J.prepare(`SELECT id FROM ${O} LIMIT 1`).first();if(x){let F=String(x.id);if(!await V.getShardMapping(F))return!0}}}catch{continue}return!1}catch{return!1}}function a(){E.clear()}function e(J){let Q=`${J}_migration_check`;E.delete(Q)}var E;var B=b(()=>{D();E=new Map});D();v();var C=null;function WJ(J){if(C=J,J.shards&&Object.keys(J.shards).length>0&&!J.disableAutoMigration)QJ(J).catch((Q)=>{console.warn("Background auto-migration failed:",Q)})}async function JJ(J){if(C=J,J.shards&&Object.keys(J.shards).length>0&&!J.disableAutoMigration)try{await QJ(J)}catch(Q){console.warn("Auto migration failed:",Q)}}async function YJ(J,Q){return await JJ(J),await Q()}async function QJ(J){try{let{autoDetectAndMigrate:Q}=await Promise.resolve().then(() => (B(),k)),U=Object.keys(J.shards);console.log(`\uD83D\uDD0D Checking ${U.length} shards for existing data...`);let Z=U.map(async(Y)=>{let V=J.shards[Y];if(!V)return null;try{let W=await Q(V,Y,J,{maxRecordsToCheck:1000});return{shardName:Y,...W}}catch(W){return console.warn(`Auto-migration failed for shard ${Y}:`,W),null}}),z=(await Promise.all(Z)).filter((Y)=>Y?.migrationPerformed);if(z.length>0){let Y=z.reduce((V,W)=>V+(W?.recordsMigrated||0),0);console.log(`\uD83C\uDF89 Auto-migration completed! Migrated ${Y} records across ${z.length} shards`),z.forEach((V)=>{if(V)console.log(` ✅ ${V.shardName}: ${V.recordsMigrated} records from ${V.tablesProcessed} tables`)})}else console.log("✅ All shards ready - no migration needed")}catch(Q){console.warn("Background auto-migration setup failed:",Q)}}function LJ(){C=null}function H(){if(!C)throw new G("CollegeDB not initialized. Call initialize() first.","NOT_INITIALIZED");return C}function VJ(J){let Q=J.trim().toUpperCase();if(Q.startsWith("SELECT")||Q.startsWith("VALUES")||Q.startsWith("TABLE")||Q.startsWith("PRAGMA")||Q.startsWith("EXPLAIN")||Q.startsWith("WITH")||Q.startsWith("SHOW"))return"read";return"write"}function OJ(J,Q){let U=J.strategy||"hash";if(typeof U==="string")return U;return U[Q]}function GJ(J,Q){if(J===Q)return 0;let U={wnam:{lat:37.7749,lon:-122.4194},enam:{lat:40.7128,lon:-74.006},weur:{lat:51.5074,lon:-0.1278},eeur:{lat:52.52,lon:13.405},apac:{lat:35.6762,lon:139.6503},oc:{lat:-33.8688,lon:151.2093},me:{lat:25.2048,lon:55.2708},af:{lat:-26.2041,lon:28.0473}},Z=U[J],$=U[Q],z=Z.lat-$.lat,Y=Z.lon-$.lon;return Math.sqrt(z*z+Y*Y)}function XJ(J){let Q=J.cf;if(!Q||!Q.country)return"wnam";let{country:U,continent:Z}=Q;if(["US","CA","MX"].includes(U)){let $=Q.region||Q.regionCode||"",z=Q.timezone||"";if($.includes("CA")||$.includes("WA")||$.includes("OR")||$.includes("NV")||$.includes("AZ")||$.includes("UT")||z.includes("Pacific")||z.includes("America/Los_Angeles"))return"wnam";return"enam"}if(["GL","PM","BM"].includes(U))return"enam";if(["GB","IE","FR","ES","PT","NL","BE","LU","CH","AT","IT"].includes(U))return"weur";if(["DE","PL","CZ","SK","HU","SI","HR","BA","RS","ME","MK","AL","BG","RO","MD","UA","BY","LT","LV","EE","FI","SE","NO","DK","IS"].includes(U))return"eeur";if(U==="RU")return"eeur";if(["JP","KR","CN","HK","TW","MO","MN","KP"].includes(U))return"apac";if(["TH","VN","SG","MY","ID","PH","BN","KH","LA","MM","TL","IN","PK","BD","LK","NP","BT","MV","AF"].includes(U))return"apac";if(["AU","NZ","PG","FJ","NC","VU","SB","WS","TO","KI","NR","PW","FM","MH","TV"].includes(U))return"oc";if(["AE","SA","QA","KW","BH","OM","YE","IQ","IR","SY","LB","JO","IL","PS","TR","CY"].includes(U))return"me";if(Z==="AF"||["EG","LY","TN","DZ","MA","SD","SS","ET","ER","DJ","SO"].includes(U))return"af";if(["KZ","UZ","TM","TJ","KG"].includes(U))return"eeur";if(Z==="SA"||["BR","AR","CL","PE","CO","VE","EC","BO","PY","UY","GY","SR","GF"].includes(U))return"enam";if(["GT","BZ","SV","HN","NI","CR","PA","CU","JM","HT","DO","PR","TT","BB","GD","VC","LC","DM","AG","KN"].includes(U))return"enam";return"wnam"}function xJ(J,Q,U,Z){let $=Q.filter((L)=>U[L]);if($.length===0){let L=0;for(let x=0;x<Z.length;x++){let F=Z.charCodeAt(x);L=(L<<5)-L+F,L=L&L}let X=Math.abs(L)%Q.length;return Q[X]}let z=$.map((L)=>{let X=U[L],x=GJ(J,X.region),F=X.priority||1,T=x-F*0.1;return{shard:L,score:T,distance:x,priority:F}});z.sort((L,X)=>L.score-X.score);let Y=z[0].score,V=z.filter((L)=>Math.abs(L.score-Y)<0.01);if(V.length===1)return V[0].shard;let W=0;for(let L=0;L<Z.length;L++){let X=Z.charCodeAt(L);W=(W<<5)-W+X,W=W&W}let O=Math.abs(W)%V.length;return V[O].shard}async function FJ(J,Q="write"){let U=H(),Z=new A(U.kv),$=await Z.getShardMapping(J);if($)return $.shard;let z=Object.keys(U.shards);if(z.length===0)throw new G("No shards configured","NO_SHARDS");for(let W of z){let O=U.shards[W];if(!O)continue;try{let{autoDetectAndMigrate:L}=await Promise.resolve().then(() => (B(),k));if((await L(O,W,U,{maxRecordsToCheck:100})).migrationPerformed){let x=await Z.getShardMapping(J);if(x)return x.shard}}catch(L){console.warn(`Auto-migration check failed for shard ${W}:`,L)}}let Y,V=OJ(U,Q);if(U.coordinator)try{let W=U.coordinator.idFromName("default"),L=await U.coordinator.get(W).fetch("http://coordinator/allocate",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({primaryKey:J,strategy:V,operationType:Q,targetRegion:U.targetRegion,shardLocations:U.shardLocations})});if(L.ok)Y=(await L.json()).shard;else Y=z[Math.floor(Math.random()*z.length)]}catch(W){console.warn("Coordinator allocation failed, falling back to local strategy:",W),Y=z[Math.floor(Math.random()*z.length)]}else switch(V){case"hash":let W=0;for(let L=0;L<J.length;L++){let X=J.charCodeAt(L);W=(W<<5)-W+X,W=W&W}let O=Math.abs(W)%z.length;Y=z[O]||z[0];break;case"location":if(!U.targetRegion){console.warn("Location strategy requires targetRegion in config, falling back to hash");let L=0;for(let x=0;x<J.length;x++){let F=J.charCodeAt(x);L=(L<<5)-L+F,L=L&L}let X=Math.abs(L)%z.length;Y=z[X]||z[0]}else Y=xJ(U.targetRegion,z,U.shardLocations||{},J);break;case"random":Y=z[Math.floor(Math.random()*z.length)]||z[0];break;default:Y=z[0];break}return await Z.setShardMapping(J,Y),Y}async function jJ(J,Q="write"){let U=H(),Z=await FJ(J,Q),$=U.shards[Z];if(!$)throw new G(`Shard ${Z} not found in configuration`,"SHARD_NOT_FOUND");return $}async function IJ(J,Q){let{createSchema:U}=await Promise.resolve().then(() => (B(),k));await U(J,Q)}async function K(J,Q){let U=VJ(Q);return(await jJ(J,U)).prepare(Q)}async function _J(J,Q,U=[]){let $=await(await K(J,Q)).bind(...U).run();if(!$.success)throw new G(`Query failed: ${$.error||"Unknown error"}`,"QUERY_FAILED");return $}async function HJ(J,Q,U=[]){let $=await(await K(J,Q)).bind(...U).all();if(!$.success)throw new G(`Query failed: ${$.error||"Unknown error"}`,"QUERY_FAILED");return $}async function TJ(J,Q,U=[]){return await(await K(J,Q)).bind(...U).first()}async function AJ(J,Q,U){let Z=H();if(!Z.shards[Q])throw new G(`Shard ${Q} not found in configuration`,"SHARD_NOT_FOUND");let $=new A(Z.kv),z=await $.getShardMapping(J);if(!z)throw new G(`No existing mapping found for primary key: ${J}`,"MAPPING_NOT_FOUND");if(z.shard!==Q){let{migrateRecord:Y}=await Promise.resolve().then(() => (B(),k)),V=Z.shards[z.shard],W=Z.shards[Q];if(!V||!W)throw new G("Source or target shard not available","SHARD_UNAVAILABLE");await Y(V,W,J,U)}await $.updateShardMapping(J,Q)}async function wJ(){let J=H();if(J.coordinator)try{let Q=J.coordinator.idFromName("default"),Z=await J.coordinator.get(Q).fetch("http://coordinator/shards");if(Z.ok)return await Z.json()}catch(Q){console.warn("Failed to get shards from coordinator:",Q)}return Object.keys(J.shards)}async function EJ(){let J=H();if(J.coordinator)try{let Z=J.coordinator.idFromName("default"),z=await J.coordinator.get(Z).fetch("http://coordinator/stats");if(z.ok)return await z.json()}catch(Z){console.warn("Failed to get stats from coordinator:",Z)}let U=await new A(J.kv).getShardKeyCounts();return Object.entries(J.shards).map(([Z,$])=>({binding:Z,count:U[Z]||0}))}async function MJ(J,Q,U=[]){let $=H().shards[J];if(!$)throw new G(`Shard ${J} not found`,"SHARD_NOT_FOUND");let z=await $.prepare(Q).bind(...U).run();if(!z.success)throw new G(`Query failed: ${z.error||"Unknown error"}`,"QUERY_FAILED");return z}async function RJ(J,Q,U=[]){let $=H().shards[J];if(!$)throw new G(`Shard ${J} not found`,"SHARD_NOT_FOUND");return await $.prepare(Q).bind(...U).all()}async function DJ(J,Q,U=[]){let $=H().shards[J];if(!$)throw new G(`Shard ${J} not found`,"SHARD_NOT_FOUND");return await $.prepare(Q).bind(...U).first()}async function PJ(){let J=H();if(await new A(J.kv).clearAllMappings(),J.coordinator)try{let U=J.coordinator.idFromName("default");await J.coordinator.get(U).fetch("http://coordinator/flush",{method:"POST"})}catch(U){console.warn("Failed to flush coordinator:",U)}}D();class y{state;constructor(J){this.state=J}async getState(){return await this.state.storage.get("coordinator_state")||{knownShards:[],shardStats:{},strategy:"round-robin",roundRobinIndex:0}}async saveState(J){await this.state.storage.put("coordinator_state",J)}async fetch(J){let U=new URL(J.url).pathname,Z=J.method;try{switch(`${Z} ${U}`){case"GET /shards":return this.handleListShards();case"POST /shards":return this.handleAddShard(J);case"DELETE /shards":return this.handleRemoveShard(J);case"GET /stats":return this.handleGetStats();case"POST /stats":return this.handleUpdateStats(J);case"POST /allocate":return this.handleAllocateShard(J);case"POST /flush":return this.handleFlush();case"GET /health":return new Response("OK",{status:200});default:return new Response("Not Found",{status:404})}}catch($){return console.error("ShardCoordinator error:",$),new Response("Internal Server Error",{status:500})}}async handleListShards(){let J=await this.getState();return new Response(JSON.stringify(J.knownShards),{headers:{"Content-Type":"application/json"}})}async handleAddShard(J){let{shard:Q}=await J.json();if(!Q||typeof Q!=="string")return new Response(JSON.stringify({error:"Missing or invalid shard parameter"}),{status:400,headers:{"Content-Type":"application/json"}});let U=await this.getState();if(!U.knownShards.includes(Q))U.knownShards.push(Q),U.shardStats[Q]={binding:Q,count:0,lastUpdated:Date.now()},await this.saveState(U);return new Response(JSON.stringify({success:!0}),{headers:{"Content-Type":"application/json"}})}async handleRemoveShard(J){let{shard:Q}=await J.json();if(!Q||typeof Q!=="string")return new Response(JSON.stringify({error:"Missing or invalid shard parameter"}),{status:400,headers:{"Content-Type":"application/json"}});let U=await this.getState(),Z=U.knownShards.indexOf(Q);if(Z>-1){if(U.knownShards.splice(Z,1),delete U.shardStats[Q],U.roundRobinIndex>=U.knownShards.length)U.roundRobinIndex=0;await this.saveState(U)}return new Response(JSON.stringify({success:!0}),{headers:{"Content-Type":"application/json"}})}async handleGetStats(){let J=await this.getState(),Q=Object.values(J.shardStats);return new Response(JSON.stringify(Q),{headers:{"Content-Type":"application/json"}})}async handleUpdateStats(J){let{shard:Q,count:U}=await J.json();if(!Q||typeof Q!=="string")return new Response(JSON.stringify({error:"Missing or invalid shard parameter"}),{status:400,headers:{"Content-Type":"application/json"}});if(U===void 0||typeof U!=="number")return new Response(JSON.stringify({error:"Missing or invalid count parameter"}),{status:400,headers:{"Content-Type":"application/json"}});let Z=await this.getState();if(Z.shardStats[Q])Z.shardStats[Q].count=U,Z.shardStats[Q].lastUpdated=Date.now(),await this.saveState(Z);return new Response(JSON.stringify({success:!0}),{headers:{"Content-Type":"application/json"}})}async handleAllocateShard(J){let{primaryKey:Q,strategy:U,operationType:Z}=await J.json();if(!Q||typeof Q!=="string")return new Response(JSON.stringify({error:"Missing or invalid primaryKey parameter"}),{status:400,headers:{"Content-Type":"application/json"}});let $=await this.getState();if($.knownShards.length===0)return new Response(JSON.stringify({error:"No shards available"}),{status:400,headers:{"Content-Type":"application/json"}});let z=this.resolveStrategy($.strategy,U,Z||"write"),Y=this.selectShard(Q,$,z);if(z==="round-robin")$.roundRobinIndex=($.roundRobinIndex+1)%$.knownShards.length,await this.saveState($);return new Response(JSON.stringify({shard:Y}),{headers:{"Content-Type":"application/json"}})}async handleFlush(){return await this.state.storage.deleteAll(),new Response(JSON.stringify({success:!0}),{headers:{"Content-Type":"application/json"}})}resolveStrategy(J,Q,U="write"){if(Q)return Q;if(typeof J==="string")return J;return J[U]}selectShard(J,Q,U){let Z=Q.knownShards;if(Z.length===0)throw new G("No shards available","NO_SHARDS");switch(U){case"round-robin":return Z[Q.roundRobinIndex]??Z[0];case"random":return Z[Math.floor(Math.random()*Z.length)];case"hash":let $=0;for(let W=0;W<J.length;W++){let O=J.charCodeAt(W);$=($<<5)-$+O,$=$&$}let z=Math.abs($)%Z.length;return Z[z];case"location":let Y=0;for(let W=0;W<J.length;W++){let O=J.charCodeAt(W);Y=(Y<<5)-Y+O,Y=Y&Y}let V=Math.abs(Y)%Z.length;return Z[V];default:return Z[0]}}async incrementShardCount(J){let Q=await this.getState();if(Q.shardStats[J])Q.shardStats[J].count++,Q.shardStats[J].lastUpdated=Date.now(),await this.saveState(Q)}async decrementShardCount(J){let Q=await this.getState();if(Q.shardStats[J]&&Q.shardStats[J].count>0)Q.shardStats[J].count--,Q.shardStats[J].lastUpdated=Date.now(),await this.saveState(Q)}}if(typeof global!=="undefined"){class J{data=new Map;async get(Z){return this.data.get(Z)}async put(Z,$){this.data.set(Z,$)}async delete(Z){return this.data.delete(Z)}async deleteAll(){this.data.clear()}async list(Z){if(!Z?.prefix)return new Map(this.data);let $=new Map;for(let[z,Y]of this.data.entries())if(z.startsWith(Z.prefix))$.set(z,Y);return $}}class Q{storage;constructor(){this.storage=new J}}class U{coordinator;mockState;constructor(){this.mockState=new Q,this.coordinator=new y(this.mockState)}async testShardAllocation(){await this.coordinator.fetch(new Request("http://test/shards",{method:"POST",body:JSON.stringify({shard:"db-east"})})),await this.coordinator.fetch(new Request("http://test/shards",{method:"POST",body:JSON.stringify({shard:"db-west"})}));let $=await(await this.coordinator.fetch(new Request("http://test/allocate",{method:"POST",body:JSON.stringify({primaryKey:"user-1",strategy:"round-robin"})}))).json(),Y=await(await this.coordinator.fetch(new Request("http://test/allocate",{method:"POST",body:JSON.stringify({primaryKey:"user-2",strategy:"round-robin"})}))).json();console.assert($.shard!==Y.shard,"Round-robin should alternate shards");let W=await(await this.coordinator.fetch(new Request("http://test/allocate",{method:"POST",body:JSON.stringify({primaryKey:"consistent-key",strategy:"hash"})}))).json(),L=await(await this.coordinator.fetch(new Request("http://test/allocate",{method:"POST",body:JSON.stringify({primaryKey:"consistent-key",strategy:"hash"})}))).json();console.assert(W.shard===L.shard,"Hash allocation should be consistent"),console.log("✅ Shard allocation tests passed")}async testShardStats(){await this.coordinator.fetch(new Request("http://test/flush",{method:"POST"})),await this.coordinator.fetch(new Request("http://test/shards",{method:"POST",body:JSON.stringify({shard:"db-stats-test"})})),await this.coordinator.fetch(new Request("http://test/stats",{method:"POST",body:JSON.stringify({shard:"db-stats-test",count:42})}));let $=await(await this.coordinator.fetch(new Request("http://test/stats",{method:"GET"}))).json();console.assert($.length===1,"Should have one shard stat"),console.assert($[0]?.binding==="db-stats-test","Should have correct binding name"),console.assert($[0]?.count===42,"Should have correct count"),console.log("✅ Shard stats tests passed")}async testErrorHandling(){await this.coordinator.fetch(new Request("http://test/flush",{method:"POST"}));let Z=await this.coordinator.fetch(new Request("http://test/allocate",{method:"POST",body:JSON.stringify({primaryKey:"test-key"})}));console.assert(Z.status===400,"Should return 400 for no shards available");let $=await this.coordinator.fetch(new Request("http://test/invalid",{method:"GET"}));console.assert($.status===404,"Should return 404 for invalid endpoint"),console.log("✅ Error handling tests passed")}async testCountManagement(){await this.coordinator.fetch(new Request("http://test/shards",{method:"POST",body:JSON.stringify({shard:"db-count-test"})})),await this.coordinator.incrementShardCount("db-count-test"),await this.coordinator.incrementShardCount("db-count-test");let Z=await this.coordinator.fetch(new Request("http://test/stats",{method:"GET"})),$=await Z.json(),z=$.find((V)=>V.binding==="db-count-test");console.assert(z?.count===2,"Count should be 2 after two increments"),await this.coordinator.decrementShardCount("db-count-test"),Z=await this.coordinator.fetch(new Request("http://test/stats",{method:"GET"})),$=await Z.json();let Y=$.find((V)=>V.binding==="db-count-test");console.assert(Y?.count===1,"Count should be 1 after decrement"),console.log("✅ Count management tests passed")}async runAllTests(){console.log("\uD83E\uDDEA Running ShardCoordinator tests...");try{return await this.testShardAllocation(),await this.testShardStats(),await this.testErrorHandling(),await this.testCountManagement(),console.log("\uD83C\uDF89 All ShardCoordinator tests passed!"),!0}catch(Z){return console.error("❌ ShardCoordinator tests failed:",Z),!1}}}globalThis.testShardCoordinator=()=>new U}D();v();B();export{N as validateTableForSharding,c as schemaExists,MJ as runShard,_J as run,LJ as resetConfig,AJ as reassignShard,K as prepare,o as migrateRecord,P as listTables,wJ as listKnownShards,t as integrateExistingDatabase,JJ as initializeAsync,WJ as initialize,EJ as getShardStats,XJ as getClosestRegionFromIP,PJ as flush,DJ as firstShard,TJ as first,n as dropSchema,S as discoverExistingPrimaryKeys,d as createSchemaAcrossShards,IJ as createSchema,i as createMappingsForExistingKeys,YJ as collegedb,e as clearShardMigrationCache,a as clearMigrationCache,r as checkMigrationNeeded,s as autoDetectAndMigrate,RJ as allShard,HJ as all,y as ShardCoordinator,A as KVShardMapper,G as CollegeDBError};
|
|
16
|
+
`).run()}if(q.set(j,!0),A)console.log(`Auto-migration completed for shard ${Z}: ${x} records from ${F} tables`)}catch(S){Y.push(`Auto-migration error: ${S}`)}return{migrationNeeded:H,migrationPerformed:A,recordsMigrated:x,tablesProcessed:F,issues:Y}}async function QJ(J,Z,$){let V=`${Z}_migration_check`;if(q.has(V))return!1;try{let Q=await C(J);if(Q.includes("shard_mappings"))return q.set(V,!0),!1;let{KVShardMapper:O}=await Promise.resolve().then(() => (f(),y)),z=new O($.kv,{hashShardMappings:$.hashShardMappings}),U=Q.filter((j)=>j!=="shard_mappings"&&!j.startsWith("sqlite_")&&j!=="sqlite_sequence");for(let j of U.slice(0,3))try{if(((await J.prepare(`SELECT COUNT(*) as count FROM ${j} LIMIT 1`).first())?.count||0)>0){let F=await J.prepare(`SELECT id FROM ${j} LIMIT 1`).first();if(F){let H=String(F.id);if(!await z.getShardMapping(H))return!0}}}catch{continue}return!1}catch{return!1}}function VJ(){q.clear()}function WJ(J){let Z=`${J}_migration_check`;q.delete(Z)}var q;var M=m(()=>{k();q=new Map});k();f();var u=null;function xJ(J){if(u=J,J.shards&&Object.keys(J.shards).length>0&&!J.disableAutoMigration)UJ(J).catch((Z)=>{console.warn("Background auto-migration failed:",Z)})}async function OJ(J){if(u=J,J.shards&&Object.keys(J.shards).length>0&&!J.disableAutoMigration)try{await UJ(J)}catch(Z){console.warn("Auto migration failed:",Z)}}async function FJ(J,Z){return await OJ(J),await Z()}async function UJ(J){try{let{autoDetectAndMigrate:Z}=await Promise.resolve().then(() => (M(),b)),$=Object.keys(J.shards);console.log(`\uD83D\uDD0D Checking ${$.length} shards for existing data...`);let V=$.map(async(O)=>{let z=J.shards[O];if(!z)return null;try{let U=await Z(z,O,J,{maxRecordsToCheck:1000});return{shardName:O,...U}}catch(U){return console.warn(`Auto-migration failed for shard ${O}:`,U),null}}),W=(await Promise.all(V)).filter((O)=>O?.migrationPerformed);if(W.length>0){let O=W.reduce((z,U)=>z+(U?.recordsMigrated||0),0);console.log(`\uD83C\uDF89 Auto-migration completed! Migrated ${O} records across ${W.length} shards`),W.forEach((z)=>{if(z)console.log(` ✅ ${z.shardName}: ${z.recordsMigrated} records from ${z.tablesProcessed} tables`)})}else console.log("✅ All shards ready - no migration needed")}catch(Z){console.warn("Background auto-migration setup failed:",Z)}}function GJ(){u=null}function v(){if(!u)throw new G("CollegeDB not initialized. Call initialize() first.","NOT_INITIALIZED");return u}function LJ(J){let Z=J.trim().toUpperCase();if(Z.startsWith("SELECT")||Z.startsWith("VALUES")||Z.startsWith("TABLE")||Z.startsWith("PRAGMA")||Z.startsWith("EXPLAIN")||Z.startsWith("WITH")||Z.startsWith("SHOW"))return"read";return"write"}function XJ(J,Z){let $=J.strategy||"hash";if(typeof $==="string")return $;return $[Z]}function HJ(J,Z){if(J===Z)return 0;let $={wnam:{lat:37.7749,lon:-122.4194},enam:{lat:40.7128,lon:-74.006},weur:{lat:51.5074,lon:-0.1278},eeur:{lat:52.52,lon:13.405},apac:{lat:35.6762,lon:139.6503},oc:{lat:-33.8688,lon:151.2093},me:{lat:25.2048,lon:55.2708},af:{lat:-26.2041,lon:28.0473}},V=$[J],Q=$[Z],W=V.lat-Q.lat,O=V.lon-Q.lon;return Math.sqrt(W*W+O*O)}function TJ(J){let Z=J.cf;if(!Z||!Z.country)return"wnam";let{country:$,continent:V}=Z;if(["US","CA","MX"].includes($)){let Q=Z.region||Z.regionCode||"",W=Z.timezone||"";if(Q.includes("CA")||Q.includes("WA")||Q.includes("OR")||Q.includes("NV")||Q.includes("AZ")||Q.includes("UT")||W.includes("Pacific")||W.includes("America/Los_Angeles"))return"wnam";return"enam"}if(["GL","PM","BM"].includes($))return"enam";if(["GB","IE","FR","ES","PT","NL","BE","LU","CH","AT","IT"].includes($))return"weur";if(["DE","PL","CZ","SK","HU","SI","HR","BA","RS","ME","MK","AL","BG","RO","MD","UA","BY","LT","LV","EE","FI","SE","NO","DK","IS"].includes($))return"eeur";if($==="RU")return"eeur";if(["JP","KR","CN","HK","TW","MO","MN","KP"].includes($))return"apac";if(["TH","VN","SG","MY","ID","PH","BN","KH","LA","MM","TL","IN","PK","BD","LK","NP","BT","MV","AF"].includes($))return"apac";if(["AU","NZ","PG","FJ","NC","VU","SB","WS","TO","KI","NR","PW","FM","MH","TV"].includes($))return"oc";if(["AE","SA","QA","KW","BH","OM","YE","IQ","IR","SY","LB","JO","IL","PS","TR","CY"].includes($))return"me";if(V==="AF"||["EG","LY","TN","DZ","MA","SD","SS","ET","ER","DJ","SO"].includes($))return"af";if(["KZ","UZ","TM","TJ","KG"].includes($))return"eeur";if(V==="SA"||["BR","AR","CL","PE","CO","VE","EC","BO","PY","UY","GY","SR","GF"].includes($))return"enam";if(["GT","BZ","SV","HN","NI","CR","PA","CU","JM","HT","DO","PR","TT","BB","GD","VC","LC","DM","AG","KN"].includes($))return"enam";return"wnam"}function wJ(J,Z,$,V){let Q=Z.filter((Y)=>$[Y]);if(Q.length===0){let Y=0;for(let F=0;F<V.length;F++){let H=V.charCodeAt(F);Y=(Y<<5)-Y+H,Y=Y&Y}let x=Math.abs(Y)%Z.length;return Z[x]}let W=Q.map((Y)=>{let x=$[Y],F=HJ(J,x.region),H=x.priority||1,A=F-H*0.1;return{shard:Y,score:A,distance:F,priority:H}});W.sort((Y,x)=>Y.score-x.score);let O=W[0].score,z=W.filter((Y)=>Math.abs(Y.score-O)<0.01);if(z.length===1)return z[0].shard;let U=0;for(let Y=0;Y<V.length;Y++){let x=V.charCodeAt(Y);U=(U<<5)-U+x,U=U&U}let j=Math.abs(U)%z.length;return z[j].shard}async function AJ(J,Z="write"){let $=v(),V=new E($.kv,{hashShardMappings:$.hashShardMappings}),Q=await V.getShardMapping(J);if(Q)return Q.shard;let W=Object.keys($.shards);if(W.length===0)throw new G("No shards configured","NO_SHARDS");for(let U of W){let j=$.shards[U];if(!j)continue;try{let{autoDetectAndMigrate:Y}=await Promise.resolve().then(() => (M(),b));if((await Y(j,U,$,{maxRecordsToCheck:100})).migrationPerformed){let F=await V.getShardMapping(J);if(F)return F.shard}}catch(Y){console.warn(`Auto-migration check failed for shard ${U}:`,Y)}}let O,z=XJ($,Z);if($.coordinator)try{let U=$.coordinator.idFromName("default"),Y=await $.coordinator.get(U).fetch("http://coordinator/allocate",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({primaryKey:J,strategy:z,operationType:Z,targetRegion:$.targetRegion,shardLocations:$.shardLocations})});if(Y.ok)O=(await Y.json()).shard;else O=W[Math.floor(Math.random()*W.length)]}catch(U){console.warn("Coordinator allocation failed, falling back to local strategy:",U),O=W[Math.floor(Math.random()*W.length)]}else switch(z){case"hash":let U=0;for(let Y=0;Y<J.length;Y++){let x=J.charCodeAt(Y);U=(U<<5)-U+x,U=U&U}let j=Math.abs(U)%W.length;O=W[j]||W[0];break;case"location":if(!$.targetRegion){console.warn("Location strategy requires targetRegion in config, falling back to hash");let Y=0;for(let F=0;F<J.length;F++){let H=J.charCodeAt(F);Y=(Y<<5)-Y+H,Y=Y&Y}let x=Math.abs(Y)%W.length;O=W[x]||W[0]}else O=wJ($.targetRegion,W,$.shardLocations||{},J);break;case"random":O=W[Math.floor(Math.random()*W.length)]||W[0];break;default:O=W[0];break}return await V.setShardMapping(J,O),O}async function DJ(J,Z="write"){let $=v(),V=await AJ(J,Z),Q=$.shards[V];if(!Q)throw new G(`Shard ${V} not found in configuration`,"SHARD_NOT_FOUND");return Q}async function BJ(J,Z){let{createSchema:$}=await Promise.resolve().then(() => (M(),b));await $(J,Z)}async function h(J,Z){let $=LJ(Z);return(await DJ(J,$)).prepare(Z)}async function vJ(J,Z,$=[]){let Q=await(await h(J,Z)).bind(...$).run();if(!Q.success)throw new G(`Query failed: ${Q.error||"Unknown error"}`,"QUERY_FAILED");return Q}async function RJ(J,Z,$=[]){let Q=await(await h(J,Z)).bind(...$).all();if(!Q.success)throw new G(`Query failed: ${Q.error||"Unknown error"}`,"QUERY_FAILED");return Q}async function EJ(J,Z,$=[]){return await(await h(J,Z)).bind(...$).first()}async function IJ(J,Z,$){let V=v();if(!V.shards[Z])throw new G(`Shard ${Z} not found in configuration`,"SHARD_NOT_FOUND");let Q=new E(V.kv,{hashShardMappings:V.hashShardMappings}),W=await Q.getShardMapping(J);if(!W)throw new G(`No existing mapping found for primary key: ${J}`,"MAPPING_NOT_FOUND");if(W.shard!==Z){let{migrateRecord:O}=await Promise.resolve().then(() => (M(),b)),z=V.shards[W.shard],U=V.shards[Z];if(!z||!U)throw new G("Source or target shard not available","SHARD_UNAVAILABLE");await O(z,U,J,$)}await Q.updateShardMapping(J,Z)}async function _J(){let J=v();if(J.coordinator)try{let Z=J.coordinator.idFromName("default"),V=await J.coordinator.get(Z).fetch("http://coordinator/shards");if(V.ok)return await V.json()}catch(Z){console.warn("Failed to get shards from coordinator:",Z)}return Object.keys(J.shards)}async function qJ(){let J=v();if(J.coordinator)try{let V=J.coordinator.idFromName("default"),W=await J.coordinator.get(V).fetch("http://coordinator/stats");if(W.ok)return await W.json()}catch(V){console.warn("Failed to get stats from coordinator:",V)}let $=await new E(J.kv,{hashShardMappings:J.hashShardMappings}).getShardKeyCounts();return Object.entries(J.shards).map(([V,Q])=>({binding:V,count:$[V]||0}))}async function PJ(J,Z,$=[]){let Q=v().shards[J];if(!Q)throw new G(`Shard ${J} not found`,"SHARD_NOT_FOUND");let W=await Q.prepare(Z).bind(...$).run();if(!W.success)throw new G(`Query failed: ${W.error||"Unknown error"}`,"QUERY_FAILED");return W}async function kJ(J,Z,$=[]){let Q=v().shards[J];if(!Q)throw new G(`Shard ${J} not found`,"SHARD_NOT_FOUND");return await Q.prepare(Z).bind(...$).all()}async function CJ(J,Z,$=[]){let Q=v().shards[J];if(!Q)throw new G(`Shard ${J} not found`,"SHARD_NOT_FOUND");return await Q.prepare(Z).bind(...$).first()}async function MJ(){let J=v();if(await new E(J.kv,{hashShardMappings:J.hashShardMappings}).clearAllMappings(),J.coordinator)try{let $=J.coordinator.idFromName("default");await J.coordinator.get($).fetch("http://coordinator/flush",{method:"POST"})}catch($){console.warn("Failed to flush coordinator:",$)}}k();class o{state;constructor(J){this.state=J}async getState(){return await this.state.storage.get("coordinator_state")||{knownShards:[],shardStats:{},strategy:"round-robin",roundRobinIndex:0}}async saveState(J){await this.state.storage.put("coordinator_state",J)}async fetch(J){let $=new URL(J.url).pathname,V=J.method;try{switch(`${V} ${$}`){case"GET /shards":return this.handleListShards();case"POST /shards":return this.handleAddShard(J);case"DELETE /shards":return this.handleRemoveShard(J);case"GET /stats":return this.handleGetStats();case"POST /stats":return this.handleUpdateStats(J);case"POST /allocate":return this.handleAllocateShard(J);case"POST /flush":return this.handleFlush();case"GET /health":return new Response("OK",{status:200});default:return new Response("Not Found",{status:404})}}catch(Q){return console.error("ShardCoordinator error:",Q),new Response("Internal Server Error",{status:500})}}async handleListShards(){let J=await this.getState();return new Response(JSON.stringify(J.knownShards),{headers:{"Content-Type":"application/json"}})}async handleAddShard(J){let{shard:Z}=await J.json();if(!Z||typeof Z!=="string")return new Response(JSON.stringify({error:"Missing or invalid shard parameter"}),{status:400,headers:{"Content-Type":"application/json"}});let $=await this.getState();if(!$.knownShards.includes(Z))$.knownShards.push(Z),$.shardStats[Z]={binding:Z,count:0,lastUpdated:Date.now()},await this.saveState($);return new Response(JSON.stringify({success:!0}),{headers:{"Content-Type":"application/json"}})}async handleRemoveShard(J){let{shard:Z}=await J.json();if(!Z||typeof Z!=="string")return new Response(JSON.stringify({error:"Missing or invalid shard parameter"}),{status:400,headers:{"Content-Type":"application/json"}});let $=await this.getState(),V=$.knownShards.indexOf(Z);if(V>-1){if($.knownShards.splice(V,1),delete $.shardStats[Z],$.roundRobinIndex>=$.knownShards.length)$.roundRobinIndex=0;await this.saveState($)}return new Response(JSON.stringify({success:!0}),{headers:{"Content-Type":"application/json"}})}async handleGetStats(){let J=await this.getState(),Z=Object.values(J.shardStats);return new Response(JSON.stringify(Z),{headers:{"Content-Type":"application/json"}})}async handleUpdateStats(J){let{shard:Z,count:$}=await J.json();if(!Z||typeof Z!=="string")return new Response(JSON.stringify({error:"Missing or invalid shard parameter"}),{status:400,headers:{"Content-Type":"application/json"}});if($===void 0||typeof $!=="number")return new Response(JSON.stringify({error:"Missing or invalid count parameter"}),{status:400,headers:{"Content-Type":"application/json"}});let V=await this.getState();if(V.shardStats[Z])V.shardStats[Z].count=$,V.shardStats[Z].lastUpdated=Date.now(),await this.saveState(V);return new Response(JSON.stringify({success:!0}),{headers:{"Content-Type":"application/json"}})}async handleAllocateShard(J){let{primaryKey:Z,strategy:$,operationType:V}=await J.json();if(!Z||typeof Z!=="string")return new Response(JSON.stringify({error:"Missing or invalid primaryKey parameter"}),{status:400,headers:{"Content-Type":"application/json"}});let Q=await this.getState();if(Q.knownShards.length===0)return new Response(JSON.stringify({error:"No shards available"}),{status:400,headers:{"Content-Type":"application/json"}});let W=this.resolveStrategy(Q.strategy,$,V||"write"),O=this.selectShard(Z,Q,W);if(W==="round-robin")Q.roundRobinIndex=(Q.roundRobinIndex+1)%Q.knownShards.length,await this.saveState(Q);return new Response(JSON.stringify({shard:O}),{headers:{"Content-Type":"application/json"}})}async handleFlush(){return await this.state.storage.deleteAll(),new Response(JSON.stringify({success:!0}),{headers:{"Content-Type":"application/json"}})}resolveStrategy(J,Z,$="write"){if(Z)return Z;if(typeof J==="string")return J;return J[$]}selectShard(J,Z,$){let V=Z.knownShards;if(V.length===0)throw new G("No shards available","NO_SHARDS");switch($){case"round-robin":return V[Z.roundRobinIndex]??V[0];case"random":return V[Math.floor(Math.random()*V.length)];case"hash":let Q=0;for(let U=0;U<J.length;U++){let j=J.charCodeAt(U);Q=(Q<<5)-Q+j,Q=Q&Q}let W=Math.abs(Q)%V.length;return V[W];case"location":let O=0;for(let U=0;U<J.length;U++){let j=J.charCodeAt(U);O=(O<<5)-O+j,O=O&O}let z=Math.abs(O)%V.length;return V[z];default:return V[0]}}async incrementShardCount(J){let Z=await this.getState();if(Z.shardStats[J])Z.shardStats[J].count++,Z.shardStats[J].lastUpdated=Date.now(),await this.saveState(Z)}async decrementShardCount(J){let Z=await this.getState();if(Z.shardStats[J]&&Z.shardStats[J].count>0)Z.shardStats[J].count--,Z.shardStats[J].lastUpdated=Date.now(),await this.saveState(Z)}}if(typeof global!=="undefined"){class J{data=new Map;async get(V){return this.data.get(V)}async put(V,Q){this.data.set(V,Q)}async delete(V){return this.data.delete(V)}async deleteAll(){this.data.clear()}async list(V){if(!V?.prefix)return new Map(this.data);let Q=new Map;for(let[W,O]of this.data.entries())if(W.startsWith(V.prefix))Q.set(W,O);return Q}}class Z{storage;constructor(){this.storage=new J}}class ${coordinator;mockState;constructor(){this.mockState=new Z,this.coordinator=new o(this.mockState)}async testShardAllocation(){await this.coordinator.fetch(new Request("http://test/shards",{method:"POST",body:JSON.stringify({shard:"db-east"})})),await this.coordinator.fetch(new Request("http://test/shards",{method:"POST",body:JSON.stringify({shard:"db-west"})}));let Q=await(await this.coordinator.fetch(new Request("http://test/allocate",{method:"POST",body:JSON.stringify({primaryKey:"user-1",strategy:"round-robin"})}))).json(),O=await(await this.coordinator.fetch(new Request("http://test/allocate",{method:"POST",body:JSON.stringify({primaryKey:"user-2",strategy:"round-robin"})}))).json();console.assert(Q.shard!==O.shard,"Round-robin should alternate shards");let U=await(await this.coordinator.fetch(new Request("http://test/allocate",{method:"POST",body:JSON.stringify({primaryKey:"consistent-key",strategy:"hash"})}))).json(),Y=await(await this.coordinator.fetch(new Request("http://test/allocate",{method:"POST",body:JSON.stringify({primaryKey:"consistent-key",strategy:"hash"})}))).json();console.assert(U.shard===Y.shard,"Hash allocation should be consistent"),console.log("✅ Shard allocation tests passed")}async testShardStats(){await this.coordinator.fetch(new Request("http://test/flush",{method:"POST"})),await this.coordinator.fetch(new Request("http://test/shards",{method:"POST",body:JSON.stringify({shard:"db-stats-test"})})),await this.coordinator.fetch(new Request("http://test/stats",{method:"POST",body:JSON.stringify({shard:"db-stats-test",count:42})}));let Q=await(await this.coordinator.fetch(new Request("http://test/stats",{method:"GET"}))).json();console.assert(Q.length===1,"Should have one shard stat"),console.assert(Q[0]?.binding==="db-stats-test","Should have correct binding name"),console.assert(Q[0]?.count===42,"Should have correct count"),console.log("✅ Shard stats tests passed")}async testErrorHandling(){await this.coordinator.fetch(new Request("http://test/flush",{method:"POST"}));let V=await this.coordinator.fetch(new Request("http://test/allocate",{method:"POST",body:JSON.stringify({primaryKey:"test-key"})}));console.assert(V.status===400,"Should return 400 for no shards available");let Q=await this.coordinator.fetch(new Request("http://test/invalid",{method:"GET"}));console.assert(Q.status===404,"Should return 404 for invalid endpoint"),console.log("✅ Error handling tests passed")}async testCountManagement(){await this.coordinator.fetch(new Request("http://test/shards",{method:"POST",body:JSON.stringify({shard:"db-count-test"})})),await this.coordinator.incrementShardCount("db-count-test"),await this.coordinator.incrementShardCount("db-count-test");let V=await this.coordinator.fetch(new Request("http://test/stats",{method:"GET"})),Q=await V.json(),W=Q.find((z)=>z.binding==="db-count-test");console.assert(W?.count===2,"Count should be 2 after two increments"),await this.coordinator.decrementShardCount("db-count-test"),V=await this.coordinator.fetch(new Request("http://test/stats",{method:"GET"})),Q=await V.json();let O=Q.find((z)=>z.binding==="db-count-test");console.assert(O?.count===1,"Count should be 1 after decrement"),console.log("✅ Count management tests passed")}async runAllTests(){console.log("\uD83E\uDDEA Running ShardCoordinator tests...");try{return await this.testShardAllocation(),await this.testShardStats(),await this.testErrorHandling(),await this.testCountManagement(),console.log("\uD83C\uDF89 All ShardCoordinator tests passed!"),!0}catch(V){return console.error("❌ ShardCoordinator tests failed:",V),!1}}}globalThis.testShardCoordinator=()=>new $}k();f();M();export{g as validateTableForSharding,d as schemaExists,PJ as runShard,vJ as run,GJ as resetConfig,IJ as reassignShard,h as prepare,e as migrateRecord,C as listTables,_J as listKnownShards,ZJ as integrateExistingDatabase,OJ as initializeAsync,xJ as initialize,qJ as getShardStats,TJ as getClosestRegionFromIP,MJ as flush,CJ as firstShard,EJ as first,a as dropSchema,c as discoverExistingPrimaryKeys,r as createSchemaAcrossShards,BJ as createSchema,JJ as createMappingsForExistingKeys,FJ as collegedb,WJ as clearShardMigrationCache,VJ as clearMigrationCache,QJ as checkMigrationNeeded,$J as autoDetectAndMigrate,kJ as allShard,RJ as all,o as ShardCoordinator,E as KVShardMapper,G as CollegeDBError};
|
|
17
17
|
|
|
18
|
-
//# debugId=
|
|
18
|
+
//# debugId=688156402D62C8D964756E2164756E21
|
|
19
19
|
//# sourceMappingURL=index.js.map
|