@earth-app/collegedb 1.1.4 → 1.2.1
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 +259 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -9
- package/dist/index.js.map +6 -5
- package/dist/providers-memory.d.ts +137 -0
- package/dist/providers-memory.d.ts.map +1 -0
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -19,6 +19,7 @@ A TypeScript library for **true horizontal scaling** of SQLite-style databases p
|
|
|
19
19
|
- [Provider Adapters](#provider-adapters)
|
|
20
20
|
- [NuxtHub + Drizzle Recipes](#nuxthub--drizzle-recipes)
|
|
21
21
|
- [Sandbox Benchmarks (Docker Compose)](#sandbox-benchmarks-docker-compose)
|
|
22
|
+
- [In-Memory Providers for Testing & Development](#in-memory-providers-for-testing--development)
|
|
22
23
|
- [Basic Usage](#basic-usage)
|
|
23
24
|
- [Multi-Key Shard Mappings](#multi-key-shard-mappings)
|
|
24
25
|
- [Drop-in Replacement for Existing Databases](#drop-in-replacement-for-existing-databases)
|
|
@@ -456,6 +457,264 @@ How to read benchmark rows:
|
|
|
456
457
|
- `N/A` means the scenario was intentionally skipped in that environment.
|
|
457
458
|
- Use the detailed section for full `avg`, `p50`, `p95`, `min`, `max`, and sample count (`n`).
|
|
458
459
|
|
|
460
|
+
## In-Memory Providers for Testing & Development
|
|
461
|
+
|
|
462
|
+
CollegeDB includes lightweight, zero-dependency in-memory mock implementations of the `KVStorage` and `SQLDatabase` interfaces. These are ideal for:
|
|
463
|
+
|
|
464
|
+
- **Unit testing** without external dependencies
|
|
465
|
+
- **Integration testing** with multiple shard combinations
|
|
466
|
+
- **Local development** and rapid iteration
|
|
467
|
+
- **Sandboxed playtesting** of routing logic
|
|
468
|
+
|
|
469
|
+
The in-memory providers work in Cloudflare Workers, Node.js, and Deno environments.
|
|
470
|
+
|
|
471
|
+
### Quick Start with In-Memory Providers
|
|
472
|
+
|
|
473
|
+
```typescript
|
|
474
|
+
import { createInMemoryKVProvider, createInMemorySQLProvider, initialize, run, first } from '@earth-app/collegedb';
|
|
475
|
+
|
|
476
|
+
// Create fresh in-memory providers for each test
|
|
477
|
+
const config = {
|
|
478
|
+
kv: createInMemoryKVProvider(),
|
|
479
|
+
shards: {
|
|
480
|
+
'shard-1': createInMemorySQLProvider(),
|
|
481
|
+
'shard-2': createInMemorySQLProvider(),
|
|
482
|
+
'shard-3': createInMemorySQLProvider()
|
|
483
|
+
},
|
|
484
|
+
strategy: 'hash'
|
|
485
|
+
};
|
|
486
|
+
|
|
487
|
+
initialize(config);
|
|
488
|
+
|
|
489
|
+
// Use as normal - all operations happen in-memory
|
|
490
|
+
await run('user-1', 'INSERT INTO users (id, name, email) VALUES (?, ?, ?)', ['user-1', 'Alice', 'alice@example.com']);
|
|
491
|
+
|
|
492
|
+
const user = await first<{ id: string; name: string }>('user-1', 'SELECT id, name FROM users WHERE id = ?', ['user-1']);
|
|
493
|
+
console.log(user); // { id: 'user-1', name: 'Alice' }
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
### Unit Testing Example
|
|
497
|
+
|
|
498
|
+
```typescript
|
|
499
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
500
|
+
import { createInMemoryKVProvider, createInMemorySQLProvider, initialize, resetConfig, run, first } from '@earth-app/collegedb';
|
|
501
|
+
|
|
502
|
+
describe('User Shard Routing', () => {
|
|
503
|
+
beforeEach(() => {
|
|
504
|
+
// Fresh providers for each test
|
|
505
|
+
initialize({
|
|
506
|
+
kv: createInMemoryKVProvider(),
|
|
507
|
+
shards: {
|
|
508
|
+
'shard-1': createInMemorySQLProvider(),
|
|
509
|
+
'shard-2': createInMemorySQLProvider(),
|
|
510
|
+
'shard-3': createInMemorySQLProvider()
|
|
511
|
+
},
|
|
512
|
+
strategy: 'hash'
|
|
513
|
+
});
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
afterEach(() => {
|
|
517
|
+
resetConfig();
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
it('should insert and retrieve a user', async () => {
|
|
521
|
+
await run('user-1', 'INSERT INTO users (id, name, email) VALUES (?, ?, ?)', ['user-1', 'Alice', 'alice@example.com']);
|
|
522
|
+
|
|
523
|
+
const user = await first<{ name: string }>('user-1', 'SELECT name FROM users WHERE id = ?', ['user-1']);
|
|
524
|
+
|
|
525
|
+
expect(user?.name).toBe('Alice');
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
it('should distribute users across shards', async () => {
|
|
529
|
+
// Insert multiple users
|
|
530
|
+
for (let i = 0; i < 9; i++) {
|
|
531
|
+
await run(`user-${i}`, 'INSERT INTO users (id, name) VALUES (?, ?)', [`user-${i}`, `User ${i}`]);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// Verify each can be retrieved
|
|
535
|
+
for (let i = 0; i < 9; i++) {
|
|
536
|
+
const user = await first(`user-${i}`, 'SELECT id FROM users WHERE id = ?', [`user-${i}`]);
|
|
537
|
+
expect(user).toBeDefined();
|
|
538
|
+
}
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
it('should handle updates correctly', async () => {
|
|
542
|
+
await run('user-1', 'INSERT INTO users (id, name) VALUES (?, ?)', ['user-1', 'Alice']);
|
|
543
|
+
|
|
544
|
+
await run('user-1', 'UPDATE users SET name = ? WHERE id = ?', ['Alice Updated', 'user-1']);
|
|
545
|
+
|
|
546
|
+
const user = await first<{ name: string }>('user-1', 'SELECT name FROM users WHERE id = ?', ['user-1']);
|
|
547
|
+
|
|
548
|
+
expect(user?.name).toBe('Alice Updated');
|
|
549
|
+
});
|
|
550
|
+
});
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
### Integration Testing with Multiple Providers
|
|
554
|
+
|
|
555
|
+
Test different combinations without Docker or external services:
|
|
556
|
+
|
|
557
|
+
```typescript
|
|
558
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
559
|
+
import {
|
|
560
|
+
createInMemoryKVProvider,
|
|
561
|
+
createInMemorySQLProvider,
|
|
562
|
+
initialize,
|
|
563
|
+
resetConfig,
|
|
564
|
+
run,
|
|
565
|
+
first,
|
|
566
|
+
KVShardMapper
|
|
567
|
+
} from '@earth-app/collegedb';
|
|
568
|
+
|
|
569
|
+
describe('Multi-Provider Integration', () => {
|
|
570
|
+
it('should work with different KV/SQL combinations', async () => {
|
|
571
|
+
const combinations = [{ kvName: 'memory', sqlName: 'memory' }];
|
|
572
|
+
|
|
573
|
+
for (const combo of combinations) {
|
|
574
|
+
resetConfig();
|
|
575
|
+
|
|
576
|
+
initialize({
|
|
577
|
+
kv: createInMemoryKVProvider(),
|
|
578
|
+
shards: {
|
|
579
|
+
'shard-1': createInMemorySQLProvider(),
|
|
580
|
+
'shard-2': createInMemorySQLProvider()
|
|
581
|
+
},
|
|
582
|
+
strategy: 'hash'
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
// Test basic operations
|
|
586
|
+
await run('key-1', 'INSERT INTO data (id, value) VALUES (?, ?)', ['key-1', 'test-value']);
|
|
587
|
+
|
|
588
|
+
const row = await first('key-1', 'SELECT value FROM data WHERE id = ?', ['key-1']);
|
|
589
|
+
|
|
590
|
+
expect(row?.value).toBe('test-value');
|
|
591
|
+
}
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
it('should support lookup key mapping', async () => {
|
|
595
|
+
initialize({
|
|
596
|
+
kv: createInMemoryKVProvider(),
|
|
597
|
+
shards: { 'shard-1': createInMemorySQLProvider() },
|
|
598
|
+
strategy: 'hash'
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
const mapper = new KVShardMapper(createInMemoryKVProvider());
|
|
602
|
+
|
|
603
|
+
// Add lookup keys
|
|
604
|
+
await mapper.addLookupKeys('user-123', ['email:alice@example.com', 'username:alice']);
|
|
605
|
+
|
|
606
|
+
// Retrieve via lookup key
|
|
607
|
+
const mapping = await mapper.getShardMapping('email:alice@example.com');
|
|
608
|
+
expect(mapping?.shard).toBeDefined();
|
|
609
|
+
});
|
|
610
|
+
});
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
### Performance Testing with In-Memory Providers
|
|
614
|
+
|
|
615
|
+
Run quick performance tests locally without external dependencies:
|
|
616
|
+
|
|
617
|
+
```typescript
|
|
618
|
+
import { createInMemoryKVProvider, createInMemorySQLProvider, initialize, run } from '@earth-app/collegedb';
|
|
619
|
+
|
|
620
|
+
async function benchmarkInserts(iterations: number): Promise<number> {
|
|
621
|
+
initialize({
|
|
622
|
+
kv: createInMemoryKVProvider(),
|
|
623
|
+
shards: {
|
|
624
|
+
'shard-1': createInMemorySQLProvider(),
|
|
625
|
+
'shard-2': createInMemorySQLProvider(),
|
|
626
|
+
'shard-3': createInMemorySQLProvider()
|
|
627
|
+
},
|
|
628
|
+
strategy: 'hash'
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
const startTime = performance.now();
|
|
632
|
+
|
|
633
|
+
for (let i = 0; i < iterations; i++) {
|
|
634
|
+
const id = `perf-user-${i}`;
|
|
635
|
+
await run(id, 'INSERT INTO users (id, name) VALUES (?, ?)', [id, `User ${i}`]);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
return performance.now() - startTime;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
const duration = await benchmarkInserts(1000);
|
|
642
|
+
console.log(`1000 inserts: ${duration.toFixed(2)}ms (${((1000 / duration) * 1000).toFixed(0)} ops/sec)`);
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
### Running the Memory Provider Sandbox
|
|
646
|
+
|
|
647
|
+
CollegeDB includes a ready-made sandbox example demonstrating multiple scenarios:
|
|
648
|
+
|
|
649
|
+
```bash
|
|
650
|
+
bun run test:memory
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
This runs comprehensive benchmarks including:
|
|
654
|
+
|
|
655
|
+
- Basic CRUD operations
|
|
656
|
+
- Multi-shard data distribution
|
|
657
|
+
- KV storage operations
|
|
658
|
+
- Round-robin strategy testing
|
|
659
|
+
- JOIN query performance
|
|
660
|
+
|
|
661
|
+
### Supported Operations
|
|
662
|
+
|
|
663
|
+
Both in-memory providers support the complete CollegeDB API:
|
|
664
|
+
|
|
665
|
+
**SQLDatabase features:**
|
|
666
|
+
|
|
667
|
+
- CREATE TABLE / DROP TABLE
|
|
668
|
+
- INSERT / UPDATE / DELETE
|
|
669
|
+
- SELECT with WHERE clauses
|
|
670
|
+
- COUNT(\*) queries
|
|
671
|
+
- JOIN queries
|
|
672
|
+
- PRAGMA queries (basic support)
|
|
673
|
+
|
|
674
|
+
**KVStorage features:**
|
|
675
|
+
|
|
676
|
+
- `get()` / `put()` / `delete()`
|
|
677
|
+
- `list()` with prefix filtering and cursor-based pagination
|
|
678
|
+
- TTL/expiration support
|
|
679
|
+
|
|
680
|
+
### Limitations
|
|
681
|
+
|
|
682
|
+
The in-memory providers are intentionally simple to avoid dependencies:
|
|
683
|
+
|
|
684
|
+
- **SQL Parser**: Basic pattern matching instead of full SQL parsing; works well for standard CollegeDB patterns but may not handle complex SQL edge cases
|
|
685
|
+
- **Joins**: Supported at application level; cross-shard joins work via multiple routed queries
|
|
686
|
+
- **Transactions**: Not supported; operations are atomic per-statement
|
|
687
|
+
- **Indexes**: Created but not actually used for query optimization
|
|
688
|
+
- **Schema**: Inferred from CREATE TABLE statements; dynamic column detection based on binding order
|
|
689
|
+
|
|
690
|
+
For production use, migrate to appropriate providers (D1, Redis, PostgreSQL, etc.). For testing/development, these limitations are intentional to keep the implementation lightweight and zero-dependency.
|
|
691
|
+
|
|
692
|
+
### Migrating from In-Memory to Production Providers
|
|
693
|
+
|
|
694
|
+
When ready to migrate from testing to production:
|
|
695
|
+
|
|
696
|
+
```typescript
|
|
697
|
+
// Before (testing)
|
|
698
|
+
import { createInMemoryKVProvider, createInMemorySQLProvider } from '@earth-app/collegedb';
|
|
699
|
+
|
|
700
|
+
const config = {
|
|
701
|
+
kv: createInMemoryKVProvider(),
|
|
702
|
+
shards: { 'shard-1': createInMemorySQLProvider() }
|
|
703
|
+
};
|
|
704
|
+
|
|
705
|
+
// After (production)
|
|
706
|
+
import { createRedisKVProvider, createPostgreSQLProvider } from '@earth-app/collegedb';
|
|
707
|
+
|
|
708
|
+
const config = {
|
|
709
|
+
kv: createRedisKVProvider(redisClient),
|
|
710
|
+
shards: { 'shard-1': createPostgreSQLProvider(pgPool) }
|
|
711
|
+
};
|
|
712
|
+
|
|
713
|
+
// Rest of configuration stays the same!
|
|
714
|
+
```
|
|
715
|
+
|
|
716
|
+
The API remains identical - only the provider initialization changes.
|
|
717
|
+
|
|
459
718
|
## Basic Usage
|
|
460
719
|
|
|
461
720
|
```typescript
|
package/dist/index.d.ts
CHANGED
|
@@ -14,6 +14,7 @@ export { ShardCoordinator } from './durable';
|
|
|
14
14
|
export { CollegeDBError } from './errors';
|
|
15
15
|
export { KVShardMapper } from './kvmap';
|
|
16
16
|
export { createDrizzleSQLProvider, createHyperdriveMySQLProvider, createHyperdrivePostgresProvider, createMySQLProvider, createNuxtHubKVProvider, createPostgreSQLProvider, createRedisKVProvider, createSQLiteProvider, createValkeyKVProvider, isKVStorage, isSQLDatabase, type DrizzleClientLike, type DrizzleSqlChunkLike, type DrizzleSqlTagLike, type HyperdriveBindingLike, type HyperdriveMySQLClientFactory, type HyperdrivePostgresClientFactory, type MySQLClientLike, type NuxtHubKVLike, type PostgresClientLike, type RedisLikeClient, type SQLiteClientLike } from './providers';
|
|
17
|
+
export { InMemoryKVStorage, InMemorySQLDatabase, createInMemoryKVProvider, createInMemorySQLProvider } from './providers-memory';
|
|
17
18
|
export { autoDetectAndMigrate, checkMigrationNeeded, clearMigrationCache, clearShardMigrationCache, createMappingsForExistingKeys, createSchemaAcrossShards, discoverExistingPrimaryKeys, discoverExistingRecordsWithColumns, dropSchema, integrateExistingDatabase, listTables, migrateRecord, schemaExists, validateTableForSharding, type IntegrationOptions, type IntegrationResult, type ValidationResult } from './migrations';
|
|
18
19
|
export type { CollegeDBConfig, D1Region, Env, KVListResult, KVStorage, MixedShardingStrategy, OperationType, PreparedStatement, QueryResult, QueryResultMeta, SQLDatabase, ShardCoordinatorState, ShardLocation, ShardMapping, ShardStats, ShardingStrategy } from './types';
|
|
19
20
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,EACN,GAAG,EACH,YAAY,EACZ,kBAAkB,EAClB,cAAc,EACd,QAAQ,EACR,SAAS,EACT,KAAK,EACL,cAAc,EACd,UAAU,EACV,YAAY,EACZ,OAAO,EACP,gBAAgB,EAChB,YAAY,EACZ,KAAK,EACL,cAAc,EACd,oBAAoB,EACpB,gBAAgB,EAChB,UAAU,EACV,KAAK,EACL,sBAAsB,EACtB,qBAAqB,EACrB,uBAAuB,EACvB,yBAAyB,EACzB,aAAa,EACb,oBAAoB,EACpB,KAAK,EACL,cAAc,EACd,UAAU,EACV,UAAU,EACV,eAAe,EACf,MAAM,EACN,WAAW,EACX,eAAe,EACf,OAAO,EACP,aAAa,EACb,WAAW,EACX,GAAG,EACH,YAAY,EACZ,QAAQ,EACR,MAAM,UAAU,CAAC;AAElB,YAAY,EACX,kBAAkB,EAClB,cAAc,EACd,sBAAsB,EACtB,qBAAqB,EACrB,YAAY,EACZ,eAAe,EACf,eAAe,EACf,MAAM,UAAU,CAAC;AAGlB,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAGxC,OAAO,EACN,wBAAwB,EACxB,6BAA6B,EAC7B,gCAAgC,EAChC,mBAAmB,EACnB,uBAAuB,EACvB,wBAAwB,EACxB,qBAAqB,EACrB,oBAAoB,EACpB,sBAAsB,EACtB,WAAW,EACX,aAAa,EACb,KAAK,iBAAiB,EACtB,KAAK,mBAAmB,EACxB,KAAK,iBAAiB,EACtB,KAAK,qBAAqB,EAC1B,KAAK,4BAA4B,EACjC,KAAK,+BAA+B,EACpC,KAAK,eAAe,EACpB,KAAK,aAAa,EAClB,KAAK,kBAAkB,EACvB,KAAK,eAAe,EACpB,KAAK,gBAAgB,EACrB,MAAM,aAAa,CAAC;AAGrB,OAAO,EACN,oBAAoB,EACpB,oBAAoB,EACpB,mBAAmB,EACnB,wBAAwB,EACxB,6BAA6B,EAC7B,wBAAwB,EACxB,2BAA2B,EAC3B,kCAAkC,EAClC,UAAU,EACV,yBAAyB,EACzB,UAAU,EACV,aAAa,EACb,YAAY,EACZ,wBAAwB,EACxB,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EACrB,MAAM,cAAc,CAAC;AAGtB,YAAY,EACX,eAAe,EACf,QAAQ,EACR,GAAG,EACH,YAAY,EACZ,SAAS,EACT,qBAAqB,EACrB,aAAa,EACb,iBAAiB,EACjB,WAAW,EACX,eAAe,EACf,WAAW,EACX,qBAAqB,EACrB,aAAa,EACb,YAAY,EACZ,UAAU,EACV,gBAAgB,EAChB,MAAM,SAAS,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,EACN,GAAG,EACH,YAAY,EACZ,kBAAkB,EAClB,cAAc,EACd,QAAQ,EACR,SAAS,EACT,KAAK,EACL,cAAc,EACd,UAAU,EACV,YAAY,EACZ,OAAO,EACP,gBAAgB,EAChB,YAAY,EACZ,KAAK,EACL,cAAc,EACd,oBAAoB,EACpB,gBAAgB,EAChB,UAAU,EACV,KAAK,EACL,sBAAsB,EACtB,qBAAqB,EACrB,uBAAuB,EACvB,yBAAyB,EACzB,aAAa,EACb,oBAAoB,EACpB,KAAK,EACL,cAAc,EACd,UAAU,EACV,UAAU,EACV,eAAe,EACf,MAAM,EACN,WAAW,EACX,eAAe,EACf,OAAO,EACP,aAAa,EACb,WAAW,EACX,GAAG,EACH,YAAY,EACZ,QAAQ,EACR,MAAM,UAAU,CAAC;AAElB,YAAY,EACX,kBAAkB,EAClB,cAAc,EACd,sBAAsB,EACtB,qBAAqB,EACrB,YAAY,EACZ,eAAe,EACf,eAAe,EACf,MAAM,UAAU,CAAC;AAGlB,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAGxC,OAAO,EACN,wBAAwB,EACxB,6BAA6B,EAC7B,gCAAgC,EAChC,mBAAmB,EACnB,uBAAuB,EACvB,wBAAwB,EACxB,qBAAqB,EACrB,oBAAoB,EACpB,sBAAsB,EACtB,WAAW,EACX,aAAa,EACb,KAAK,iBAAiB,EACtB,KAAK,mBAAmB,EACxB,KAAK,iBAAiB,EACtB,KAAK,qBAAqB,EAC1B,KAAK,4BAA4B,EACjC,KAAK,+BAA+B,EACpC,KAAK,eAAe,EACpB,KAAK,aAAa,EAClB,KAAK,kBAAkB,EACvB,KAAK,eAAe,EACpB,KAAK,gBAAgB,EACrB,MAAM,aAAa,CAAC;AAGrB,OAAO,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,wBAAwB,EAAE,yBAAyB,EAAE,MAAM,oBAAoB,CAAC;AAGjI,OAAO,EACN,oBAAoB,EACpB,oBAAoB,EACpB,mBAAmB,EACnB,wBAAwB,EACxB,6BAA6B,EAC7B,wBAAwB,EACxB,2BAA2B,EAC3B,kCAAkC,EAClC,UAAU,EACV,yBAAyB,EACzB,UAAU,EACV,aAAa,EACb,YAAY,EACZ,wBAAwB,EACxB,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EACrB,MAAM,cAAc,CAAC;AAGtB,YAAY,EACX,eAAe,EACf,QAAQ,EACR,GAAG,EACH,YAAY,EACZ,SAAS,EACT,qBAAqB,EACrB,aAAa,EACb,iBAAiB,EACjB,WAAW,EACX,eAAe,EACf,WAAW,EACX,qBAAqB,EACrB,aAAa,EACb,YAAY,EACZ,UAAU,EACV,gBAAgB,EAChB,MAAM,SAAS,CAAC"}
|