@bitclaw/sqlite 1.1.0
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/LICENSE +21 -0
- package/README.md +251 -0
- package/dist/scripts/benchmark.d.ts +3 -0
- package/dist/scripts/benchmark.d.ts.map +1 -0
- package/dist/scripts/benchmark.js +286 -0
- package/dist/scripts/load-test-utils.d.ts +77 -0
- package/dist/scripts/load-test-utils.d.ts.map +1 -0
- package/dist/scripts/load-test-utils.js +235 -0
- package/dist/src/cache-lock.d.ts +25 -0
- package/dist/src/cache-lock.d.ts.map +1 -0
- package/dist/src/cache-lock.js +95 -0
- package/dist/src/connection.d.ts +26 -0
- package/dist/src/connection.d.ts.map +1 -0
- package/dist/src/connection.js +132 -0
- package/dist/src/json-cache.d.ts +89 -0
- package/dist/src/json-cache.d.ts.map +1 -0
- package/dist/src/json-cache.js +289 -0
- package/dist/src/pool.d.ts +98 -0
- package/dist/src/pool.d.ts.map +1 -0
- package/dist/src/pool.js +331 -0
- package/dist/src/prisma-immediate-tx.d.ts +23 -0
- package/dist/src/prisma-immediate-tx.d.ts.map +1 -0
- package/dist/src/prisma-immediate-tx.js +42 -0
- package/dist/src/query-logger.d.ts +21 -0
- package/dist/src/query-logger.d.ts.map +1 -0
- package/dist/src/query-logger.js +60 -0
- package/dist/src/retry.d.ts +14 -0
- package/dist/src/retry.d.ts.map +1 -0
- package/dist/src/retry.js +49 -0
- package/dist/src/ttl-cache.d.ts +57 -0
- package/dist/src/ttl-cache.d.ts.map +1 -0
- package/dist/src/ttl-cache.js +92 -0
- package/dist/src/worker.d.ts +38 -0
- package/dist/src/worker.d.ts.map +1 -0
- package/dist/src/worker.js +294 -0
- package/dist/src/write-mutex.d.ts +33 -0
- package/dist/src/write-mutex.d.ts.map +1 -0
- package/dist/src/write-mutex.js +60 -0
- package/package.json +48 -0
- package/scripts/benchmark.ts +373 -0
- package/scripts/load-test-utils.ts +370 -0
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
// packages/sqlite/src/json-cache.ts
|
|
2
|
+
// levelsio-style JSON file caching for expensive queries
|
|
3
|
+
import { randomBytes } from 'node:crypto';
|
|
4
|
+
import { promises as fs } from 'node:fs';
|
|
5
|
+
import { dirname, join } from 'node:path';
|
|
6
|
+
const isDevelopment = process.env.NODE_ENV === 'development';
|
|
7
|
+
const isCacheEntry = (data) => {
|
|
8
|
+
return (data != null &&
|
|
9
|
+
typeof data === 'object' &&
|
|
10
|
+
'value' in data &&
|
|
11
|
+
'metadata' in data &&
|
|
12
|
+
typeof data.metadata?.createdTime === 'number');
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Simple, fast JSON file cache inspired by levelsio's approach
|
|
16
|
+
* - Each cache key is a separate JSON file
|
|
17
|
+
* - Atomic writes using temp files
|
|
18
|
+
* - TTL-based expiration
|
|
19
|
+
* - Stale-while-revalidate support
|
|
20
|
+
* - No external dependencies
|
|
21
|
+
*/
|
|
22
|
+
export class JsonCache {
|
|
23
|
+
cacheDir;
|
|
24
|
+
defaultTtl;
|
|
25
|
+
pendingWrites = new Map();
|
|
26
|
+
constructor(config) {
|
|
27
|
+
this.cacheDir = config.cacheDir;
|
|
28
|
+
this.defaultTtl = config.defaultTtl ?? 300000; // 5 minutes default
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Initialize cache directory
|
|
32
|
+
*/
|
|
33
|
+
async init() {
|
|
34
|
+
try {
|
|
35
|
+
await fs.mkdir(this.cacheDir, { recursive: true });
|
|
36
|
+
if (isDevelopment) {
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
console.error('[json-cache] Failed to initialize:', error);
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Get cache file path for a key
|
|
46
|
+
*/
|
|
47
|
+
getFilePath(key) {
|
|
48
|
+
// Sanitize key for filesystem
|
|
49
|
+
const sanitized = key.replace(/[^a-z0-9-_.]/gi, '_');
|
|
50
|
+
return join(this.cacheDir, `${sanitized}.json`);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Get value from cache
|
|
54
|
+
*/
|
|
55
|
+
async get(key) {
|
|
56
|
+
const filePath = this.getFilePath(key);
|
|
57
|
+
try {
|
|
58
|
+
const data = await fs.readFile(filePath, 'utf-8');
|
|
59
|
+
const parsed = JSON.parse(data);
|
|
60
|
+
if (!isCacheEntry(parsed)) {
|
|
61
|
+
await this.delete(key);
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
const entry = parsed;
|
|
65
|
+
const now = Date.now();
|
|
66
|
+
const age = now - entry.metadata.createdTime;
|
|
67
|
+
// Check if expired
|
|
68
|
+
if (entry.metadata.ttl && age > entry.metadata.ttl) {
|
|
69
|
+
// Check stale-while-revalidate
|
|
70
|
+
if (entry.metadata.swr &&
|
|
71
|
+
age <= entry.metadata.ttl + entry.metadata.swr) {
|
|
72
|
+
// Return stale value but trigger background revalidation
|
|
73
|
+
if (isDevelopment) {
|
|
74
|
+
}
|
|
75
|
+
return entry.value;
|
|
76
|
+
}
|
|
77
|
+
// Expired and past SWR window
|
|
78
|
+
await this.delete(key);
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
return entry.value;
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
if (error instanceof Error &&
|
|
85
|
+
error.code === 'ENOENT') {
|
|
86
|
+
// File doesn't exist - cache miss
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
console.error(`[json-cache] Error reading ${key}:`, error);
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Set value in cache with atomic write
|
|
95
|
+
*/
|
|
96
|
+
async set(key, value, options) {
|
|
97
|
+
const filePath = this.getFilePath(key);
|
|
98
|
+
// If there's already a pending write for this key, wait for it
|
|
99
|
+
const pendingWrite = this.pendingWrites.get(key);
|
|
100
|
+
if (pendingWrite) {
|
|
101
|
+
await pendingWrite;
|
|
102
|
+
}
|
|
103
|
+
// Create new write promise
|
|
104
|
+
const writePromise = this._atomicWrite(filePath, value, options);
|
|
105
|
+
this.pendingWrites.set(key, writePromise);
|
|
106
|
+
try {
|
|
107
|
+
await writePromise;
|
|
108
|
+
}
|
|
109
|
+
finally {
|
|
110
|
+
this.pendingWrites.delete(key);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Atomic write using temp file
|
|
115
|
+
*/
|
|
116
|
+
async _atomicWrite(filePath, value, options) {
|
|
117
|
+
const entry = {
|
|
118
|
+
value,
|
|
119
|
+
metadata: {
|
|
120
|
+
createdTime: Date.now(),
|
|
121
|
+
ttl: options?.ttl ?? this.defaultTtl,
|
|
122
|
+
swr: options?.swr ?? null
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
// Ensure directory exists
|
|
126
|
+
const dir = dirname(filePath);
|
|
127
|
+
await fs.mkdir(dir, { recursive: true });
|
|
128
|
+
// Write to temp file first (atomic operation)
|
|
129
|
+
const tempPath = `${filePath}.${randomBytes(8).toString('hex')}.tmp`;
|
|
130
|
+
try {
|
|
131
|
+
await fs.writeFile(tempPath, JSON.stringify(entry, null, isDevelopment ? 2 : 0), 'utf-8');
|
|
132
|
+
// Atomic rename
|
|
133
|
+
await fs.rename(tempPath, filePath);
|
|
134
|
+
if (isDevelopment) {
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
// Clean up temp file if it exists
|
|
139
|
+
try {
|
|
140
|
+
await fs.unlink(tempPath);
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
// Ignore cleanup errors
|
|
144
|
+
}
|
|
145
|
+
throw error;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Delete cache entry
|
|
150
|
+
*/
|
|
151
|
+
async delete(key) {
|
|
152
|
+
const filePath = this.getFilePath(key);
|
|
153
|
+
try {
|
|
154
|
+
await fs.unlink(filePath);
|
|
155
|
+
if (isDevelopment) {
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
if (!(error instanceof Error) ||
|
|
160
|
+
error.code !== 'ENOENT') {
|
|
161
|
+
console.error(`[json-cache] Error deleting ${key}:`, error);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Check if cache entry exists
|
|
167
|
+
*/
|
|
168
|
+
async has(key) {
|
|
169
|
+
const filePath = this.getFilePath(key);
|
|
170
|
+
try {
|
|
171
|
+
await fs.access(filePath);
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Clear all cache entries
|
|
180
|
+
*/
|
|
181
|
+
async clear() {
|
|
182
|
+
try {
|
|
183
|
+
const files = await fs.readdir(this.cacheDir);
|
|
184
|
+
await Promise.all(files
|
|
185
|
+
.filter(file => file.endsWith('.json'))
|
|
186
|
+
.map(file => fs.unlink(join(this.cacheDir, file))));
|
|
187
|
+
if (isDevelopment) {
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
console.error('[json-cache] Error clearing cache:', error);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Get cache statistics
|
|
196
|
+
*/
|
|
197
|
+
async stats() {
|
|
198
|
+
try {
|
|
199
|
+
const files = await fs.readdir(this.cacheDir);
|
|
200
|
+
const jsonFiles = files.filter(file => file.endsWith('.json'));
|
|
201
|
+
return {
|
|
202
|
+
size: jsonFiles.length,
|
|
203
|
+
entries: jsonFiles.map(file => file.replace('.json', ''))
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
return { size: 0, entries: [] };
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Clean up expired entries
|
|
212
|
+
*/
|
|
213
|
+
async cleanup() {
|
|
214
|
+
try {
|
|
215
|
+
const files = await fs.readdir(this.cacheDir);
|
|
216
|
+
const jsonFiles = files.filter(file => file.endsWith('.json'));
|
|
217
|
+
let cleaned = 0;
|
|
218
|
+
for (const file of jsonFiles) {
|
|
219
|
+
const filePath = join(this.cacheDir, file);
|
|
220
|
+
try {
|
|
221
|
+
const data = await fs.readFile(filePath, 'utf-8');
|
|
222
|
+
const parsed = JSON.parse(data);
|
|
223
|
+
if (!isCacheEntry(parsed)) {
|
|
224
|
+
await fs.unlink(filePath);
|
|
225
|
+
cleaned++;
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
const entry = parsed;
|
|
229
|
+
const now = Date.now();
|
|
230
|
+
const age = now - entry.metadata.createdTime;
|
|
231
|
+
// Check if expired (including SWR window)
|
|
232
|
+
const totalTtl = entry.metadata.ttl ?? this.defaultTtl;
|
|
233
|
+
const swrWindow = entry.metadata.swr ?? 0;
|
|
234
|
+
if (age > totalTtl + swrWindow) {
|
|
235
|
+
await fs.unlink(filePath);
|
|
236
|
+
cleaned++;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
// If we can't read/parse the file, delete it
|
|
241
|
+
await fs.unlink(filePath);
|
|
242
|
+
cleaned++;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (isDevelopment && cleaned > 0) {
|
|
246
|
+
}
|
|
247
|
+
return cleaned;
|
|
248
|
+
}
|
|
249
|
+
catch (error) {
|
|
250
|
+
console.error('[json-cache] Error during cleanup:', error);
|
|
251
|
+
return 0;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Factory function to create and initialize a JSON cache instance.
|
|
257
|
+
* Awaits directory creation so the cache is ready to use immediately.
|
|
258
|
+
*/
|
|
259
|
+
export const createJsonCache = async (config) => {
|
|
260
|
+
const cache = new JsonCache(config);
|
|
261
|
+
await cache.init();
|
|
262
|
+
// Set up periodic cleanup (every hour)
|
|
263
|
+
if (process.env.NODE_ENV !== 'test') {
|
|
264
|
+
setInterval(() => {
|
|
265
|
+
cache.cleanup().catch(error => {
|
|
266
|
+
console.error('[json-cache] Cleanup failed:', error);
|
|
267
|
+
});
|
|
268
|
+
}, 3600000); // 1 hour
|
|
269
|
+
}
|
|
270
|
+
return cache;
|
|
271
|
+
};
|
|
272
|
+
/**
|
|
273
|
+
* Helper function for cachified pattern
|
|
274
|
+
*/
|
|
275
|
+
export const cachified = async (options) => {
|
|
276
|
+
const { cache, key, getFreshValue, ttl, swr } = options;
|
|
277
|
+
// Try to get from cache
|
|
278
|
+
const cached = await cache.get(key);
|
|
279
|
+
if (cached !== null) {
|
|
280
|
+
return cached;
|
|
281
|
+
}
|
|
282
|
+
// Cache miss - get fresh value
|
|
283
|
+
const freshValue = await getFreshValue();
|
|
284
|
+
// Store in cache (don't await - fire and forget)
|
|
285
|
+
cache.set(key, freshValue, { ttl, swr }).catch(error => {
|
|
286
|
+
console.error(`[json-cache] Failed to cache ${key}:`, error);
|
|
287
|
+
});
|
|
288
|
+
return freshValue;
|
|
289
|
+
};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
export type PoolMetrics = {
|
|
3
|
+
totalQueries: number;
|
|
4
|
+
activeQueries: number;
|
|
5
|
+
totalDuration: number;
|
|
6
|
+
errors: number;
|
|
7
|
+
workerStats: {
|
|
8
|
+
[workerId: string]: {
|
|
9
|
+
queries: number;
|
|
10
|
+
errors: number;
|
|
11
|
+
totalDuration: number;
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
export type PoolConfig = {
|
|
16
|
+
databasePath?: string;
|
|
17
|
+
poolSize?: number;
|
|
18
|
+
timeout?: number;
|
|
19
|
+
};
|
|
20
|
+
declare class SQLiteConnectionPool extends EventEmitter {
|
|
21
|
+
private workers;
|
|
22
|
+
private maxWorkers;
|
|
23
|
+
private workerTimeout;
|
|
24
|
+
private databasePath;
|
|
25
|
+
private pendingOperations;
|
|
26
|
+
private nextWorkerIndex;
|
|
27
|
+
private isShuttingDown;
|
|
28
|
+
private metrics;
|
|
29
|
+
constructor(config?: PoolConfig);
|
|
30
|
+
private initializePool;
|
|
31
|
+
private handleWorkerMessage;
|
|
32
|
+
private handleWorkerError;
|
|
33
|
+
private handleWorkerExit;
|
|
34
|
+
/**
|
|
35
|
+
* Execute SQL query with parameters
|
|
36
|
+
*/
|
|
37
|
+
exec<T = unknown>(sql: string, params?: unknown[]): Promise<T>;
|
|
38
|
+
/**
|
|
39
|
+
* Execute operations on a specific worker (for transactions)
|
|
40
|
+
*/
|
|
41
|
+
execOnWorker<T = unknown>(workerIndex: number, sql: string, params?: unknown[]): Promise<T>;
|
|
42
|
+
/**
|
|
43
|
+
* Get current pool metrics
|
|
44
|
+
*/
|
|
45
|
+
getMetrics(): PoolMetrics;
|
|
46
|
+
/**
|
|
47
|
+
* Get pool status for health checks
|
|
48
|
+
*/
|
|
49
|
+
getStatus(): {
|
|
50
|
+
workers: number;
|
|
51
|
+
activeQueries: number;
|
|
52
|
+
isShuttingDown: boolean;
|
|
53
|
+
totalQueries: number;
|
|
54
|
+
errorRate: number;
|
|
55
|
+
avgDuration: number;
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* Graceful shutdown of the connection pool
|
|
59
|
+
*/
|
|
60
|
+
shutdown(timeoutMs?: number): Promise<void>;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Create a new connection pool instance
|
|
64
|
+
*/
|
|
65
|
+
export declare function createPool(config?: PoolConfig): SQLiteConnectionPool;
|
|
66
|
+
/**
|
|
67
|
+
* Get the singleton connection pool instance
|
|
68
|
+
*/
|
|
69
|
+
export declare function getPool(): SQLiteConnectionPool;
|
|
70
|
+
/**
|
|
71
|
+
* Execute SQL query using the connection pool
|
|
72
|
+
*/
|
|
73
|
+
export declare function exec<T = unknown>(sql: string, params?: unknown[]): Promise<T>;
|
|
74
|
+
/**
|
|
75
|
+
* Get pool metrics for monitoring
|
|
76
|
+
*/
|
|
77
|
+
export declare function getPoolMetrics(): PoolMetrics;
|
|
78
|
+
/**
|
|
79
|
+
* Get pool status for health checks
|
|
80
|
+
*/
|
|
81
|
+
export declare function getPoolStatus(): {
|
|
82
|
+
workers: number;
|
|
83
|
+
activeQueries: number;
|
|
84
|
+
isShuttingDown: boolean;
|
|
85
|
+
totalQueries: number;
|
|
86
|
+
errorRate: number;
|
|
87
|
+
avgDuration: number;
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* Shutdown the connection pool
|
|
91
|
+
*/
|
|
92
|
+
export declare function shutdownPool(timeoutMs?: number): Promise<void>;
|
|
93
|
+
/**
|
|
94
|
+
* Execute multiple operations atomically on a single worker
|
|
95
|
+
*/
|
|
96
|
+
export declare function withTransaction<T>(operation: (execute: (sql: string, params?: unknown[]) => Promise<unknown>) => Promise<T>): Promise<T>;
|
|
97
|
+
export {};
|
|
98
|
+
//# sourceMappingURL=pool.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pool.d.ts","sourceRoot":"","sources":["../../src/pool.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AA0B3C,MAAM,MAAM,WAAW,GAAG;IACxB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE;QACX,CAAC,QAAQ,EAAE,MAAM,GAAG;YAClB,OAAO,EAAE,MAAM,CAAC;YAChB,MAAM,EAAE,MAAM,CAAC;YACf,aAAa,EAAE,MAAM,CAAC;SACvB,CAAC;KACH,CAAC;CACH,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG;IACvB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,cAAM,oBAAqB,SAAQ,YAAY;IAC7C,OAAO,CAAC,OAAO,CAAgB;IAC/B,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,aAAa,CAAS;IAC9B,OAAO,CAAC,YAAY,CAAqB;IAEzC,OAAO,CAAC,iBAAiB,CASrB;IAEJ,OAAO,CAAC,eAAe,CAAK;IAC5B,OAAO,CAAC,cAAc,CAAS;IAE/B,OAAO,CAAC,OAAO,CAMb;gBAEU,MAAM,CAAC,EAAE,UAAU;IAgB/B,OAAO,CAAC,cAAc;IAsBtB,OAAO,CAAC,mBAAmB;IAyD3B,OAAO,CAAC,iBAAiB;IAMzB,OAAO,CAAC,gBAAgB;IAMxB;;OAEG;IACG,IAAI,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,GAAE,OAAO,EAAO,GAAG,OAAO,CAAC,CAAC,CAAC;IAyCxE;;OAEG;IACG,YAAY,CAAC,CAAC,GAAG,OAAO,EAC5B,WAAW,EAAE,MAAM,EACnB,GAAG,EAAE,MAAM,EACX,MAAM,GAAE,OAAO,EAAO,GACrB,OAAO,CAAC,CAAC,CAAC;IAyCb;;OAEG;IACH,UAAU,IAAI,WAAW;IAOzB;;OAEG;IACH,SAAS;;;;;;;;IAiBT;;OAEG;IACG,QAAQ,CAAC,SAAS,SAAO,GAAG,OAAO,CAAC,IAAI,CAAC;CAqChD;AAKD;;GAEG;AACH,wBAAgB,UAAU,CAAC,MAAM,CAAC,EAAE,UAAU,GAAG,oBAAoB,CAQpE;AAED;;GAEG;AACH,wBAAgB,OAAO,IAAI,oBAAoB,CAK9C;AAED;;GAEG;AACH,wBAAsB,IAAI,CAAC,CAAC,GAAG,OAAO,EACpC,GAAG,EAAE,MAAM,EACX,MAAM,GAAE,OAAO,EAAO,GACrB,OAAO,CAAC,CAAC,CAAC,CAEZ;AAED;;GAEG;AACH,wBAAgB,cAAc,IAAI,WAAW,CAE5C;AAED;;GAEG;AACH,wBAAgB,aAAa;;;;;;;EAE5B;AAED;;GAEG;AACH,wBAAsB,YAAY,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAcpE;AAED;;GAEG;AACH,wBAAsB,eAAe,CAAC,CAAC,EACrC,SAAS,EAAE,CACT,OAAO,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,OAAO,CAAC,KAC3D,OAAO,CAAC,CAAC,CAAC,GACd,OAAO,CAAC,CAAC,CAAC,CA4BZ"}
|