duckpond 0.2.1 → 0.4.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/dist/DuckPond.d.ts +76 -96
- package/dist/DuckPond.js +29 -1
- package/dist/DuckPond.js.map +1 -1
- package/dist/LRUCache-Bg_8n4IH.js +2 -0
- package/dist/LRUCache-Bg_8n4IH.js.map +1 -0
- package/dist/cache/LRUCache.d.ts +107 -69
- package/dist/cache/LRUCache.js +1 -2
- package/dist/index.d.ts +10 -12
- package/dist/index.js +1 -2
- package/dist/types.d.ts +118 -113
- package/dist/types.js +1 -1
- package/dist/types.js.map +1 -1
- package/dist/utils/errors.d.ts +18 -18
- package/dist/utils/errors.js +2 -1
- package/dist/utils/errors.js.map +1 -1
- package/dist/utils/logger.d.ts +11 -8
- package/dist/utils/logger.js +1 -1
- package/dist/utils/logger.js.map +1 -1
- package/package.json +28 -43
- package/dist/DuckPond.d.mts +0 -100
- package/dist/DuckPond.mjs +0 -2
- package/dist/DuckPond.mjs.map +0 -1
- package/dist/cache/LRUCache.d.mts +0 -75
- package/dist/cache/LRUCache.js.map +0 -1
- package/dist/cache/LRUCache.mjs +0 -2
- package/dist/cache/LRUCache.mjs.map +0 -1
- package/dist/chunk-24M54WUC.mjs +0 -2
- package/dist/chunk-24M54WUC.mjs.map +0 -1
- package/dist/chunk-2PPSIE57.js +0 -2
- package/dist/chunk-2PPSIE57.js.map +0 -1
- package/dist/chunk-5XGN7UAV.js +0 -2
- package/dist/chunk-5XGN7UAV.js.map +0 -1
- package/dist/chunk-A5IXJPPT.mjs +0 -2
- package/dist/chunk-A5IXJPPT.mjs.map +0 -1
- package/dist/chunk-J2OQ62DV.js +0 -2
- package/dist/chunk-J2OQ62DV.js.map +0 -1
- package/dist/chunk-MLZHFU2B.js +0 -27
- package/dist/chunk-MLZHFU2B.js.map +0 -1
- package/dist/chunk-MZTKR3LR.js +0 -3
- package/dist/chunk-MZTKR3LR.js.map +0 -1
- package/dist/chunk-PAHYBGNR.mjs +0 -27
- package/dist/chunk-PAHYBGNR.mjs.map +0 -1
- package/dist/chunk-PCQEPXO3.mjs +0 -3
- package/dist/chunk-PCQEPXO3.mjs.map +0 -1
- package/dist/chunk-Q6UFPTQC.js +0 -2
- package/dist/chunk-Q6UFPTQC.js.map +0 -1
- package/dist/chunk-SZJXSB7U.mjs +0 -2
- package/dist/chunk-SZJXSB7U.mjs.map +0 -1
- package/dist/chunk-V57JCP3U.mjs +0 -2
- package/dist/chunk-V57JCP3U.mjs.map +0 -1
- package/dist/index.d.mts +0 -12
- package/dist/index.js.map +0 -1
- package/dist/index.mjs +0 -2
- package/dist/index.mjs.map +0 -1
- package/dist/types.d.mts +0 -192
- package/dist/types.mjs +0 -2
- package/dist/types.mjs.map +0 -1
- package/dist/utils/errors.d.mts +0 -41
- package/dist/utils/errors.mjs +0 -2
- package/dist/utils/errors.mjs.map +0 -1
- package/dist/utils/logger.d.mts +0 -25
- package/dist/utils/logger.mjs +0 -2
- package/dist/utils/logger.mjs.map +0 -1
package/dist/DuckPond.d.ts
CHANGED
|
@@ -1,100 +1,80 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import 'functype/either';
|
|
4
|
-
import 'functype/list';
|
|
1
|
+
import { AsyncDuckPondResult, DuckPondConfig, DuckPondResult, ListUsersResult, UserStats } from "./types.js";
|
|
2
|
+
import { DuckDBConnection } from "@duckdb/node-api";
|
|
5
3
|
|
|
6
|
-
|
|
7
|
-
* DuckPond - Multi-tenant DuckDB manager with R2/S3 storage
|
|
8
|
-
*
|
|
9
|
-
* Manages per-user DuckDB instances with:
|
|
10
|
-
* - LRU caching for active users
|
|
11
|
-
* - R2/S3 object storage integration
|
|
12
|
-
* - Functional error handling with functype Either
|
|
13
|
-
* - Automatic resource cleanup
|
|
14
|
-
*
|
|
15
|
-
* @example
|
|
16
|
-
* ```typescript
|
|
17
|
-
* const pond = new DuckPond({
|
|
18
|
-
* r2: {
|
|
19
|
-
* accountId: 'xxx',
|
|
20
|
-
* accessKeyId: 'yyy',
|
|
21
|
-
* secretAccessKey: 'zzz',
|
|
22
|
-
* bucket: 'my-bucket'
|
|
23
|
-
* }
|
|
24
|
-
* })
|
|
25
|
-
*
|
|
26
|
-
* await pond.init()
|
|
27
|
-
*
|
|
28
|
-
* const result = await pond.query('user123', 'SELECT * FROM orders')
|
|
29
|
-
* result.fold(
|
|
30
|
-
* error => console.error('Query failed:', error),
|
|
31
|
-
* rows => console.log('Results:', rows)
|
|
32
|
-
* )
|
|
33
|
-
* ```
|
|
34
|
-
*/
|
|
4
|
+
//#region src/DuckPond.d.ts
|
|
35
5
|
declare class DuckPond {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
6
|
+
private instance;
|
|
7
|
+
private cache;
|
|
8
|
+
private config;
|
|
9
|
+
private initialized;
|
|
10
|
+
constructor(config: DuckPondConfig);
|
|
11
|
+
/**
|
|
12
|
+
* Cleanup resources when a user is evicted from cache
|
|
13
|
+
* Called automatically by LRU cache dispose callback
|
|
14
|
+
*/
|
|
15
|
+
private cleanupUserResources;
|
|
16
|
+
/**
|
|
17
|
+
* Initialize DuckPond
|
|
18
|
+
* Must be called before any other operations
|
|
19
|
+
*/
|
|
20
|
+
init(): AsyncDuckPondResult<void>;
|
|
21
|
+
/**
|
|
22
|
+
* Ensure the data directory exists
|
|
23
|
+
*/
|
|
24
|
+
private ensureDataDir;
|
|
25
|
+
/**
|
|
26
|
+
* Get the database file path for a user
|
|
27
|
+
*/
|
|
28
|
+
private getUserDbPath;
|
|
29
|
+
/**
|
|
30
|
+
* Configure R2/S3 access and DuckDB extensions
|
|
31
|
+
*/
|
|
32
|
+
private setupCloudStorage;
|
|
33
|
+
/**
|
|
34
|
+
* Get a connection for a user
|
|
35
|
+
* Loads from cache or creates new database
|
|
36
|
+
*/
|
|
37
|
+
getUserConnection(userId: string): AsyncDuckPondResult<DuckDBConnection>;
|
|
38
|
+
/**
|
|
39
|
+
* Create a local file-based database connection for a user
|
|
40
|
+
*/
|
|
41
|
+
private createLocalUserConnection;
|
|
42
|
+
/**
|
|
43
|
+
* Attach a user's database based on storage strategy
|
|
44
|
+
*/
|
|
45
|
+
private attachUserDatabase;
|
|
46
|
+
/**
|
|
47
|
+
* Execute a SQL query for a user
|
|
48
|
+
* Returns Either<Error, results>
|
|
49
|
+
*/
|
|
50
|
+
query<T = unknown>(userId: string, sql: string): AsyncDuckPondResult<T[]>;
|
|
51
|
+
/**
|
|
52
|
+
* Execute SQL without returning results (DDL, DML)
|
|
53
|
+
*/
|
|
54
|
+
execute(userId: string, sql: string): AsyncDuckPondResult<void>;
|
|
55
|
+
/**
|
|
56
|
+
* Detach a user's database and free resources
|
|
57
|
+
* Cleanup happens automatically via the cache dispose callback
|
|
58
|
+
*/
|
|
59
|
+
detachUser(userId: string): DuckPondResult<void>;
|
|
60
|
+
/**
|
|
61
|
+
* Check if a user is currently attached
|
|
62
|
+
*/
|
|
63
|
+
isAttached(userId: string): boolean;
|
|
64
|
+
/**
|
|
65
|
+
* Get statistics about a user's database
|
|
66
|
+
*/
|
|
67
|
+
getUserStats(userId: string): AsyncDuckPondResult<UserStats>;
|
|
68
|
+
/**
|
|
69
|
+
* Get list of all currently cached users
|
|
70
|
+
* Returns a List of user IDs and cache statistics
|
|
71
|
+
*/
|
|
72
|
+
listUsers(): ListUsersResult;
|
|
73
|
+
/**
|
|
74
|
+
* Close DuckPond and cleanup all resources
|
|
75
|
+
*/
|
|
76
|
+
close(): DuckPondResult<void>;
|
|
98
77
|
}
|
|
99
|
-
|
|
78
|
+
//#endregion
|
|
100
79
|
export { DuckPond };
|
|
80
|
+
//# sourceMappingURL=DuckPond.d.ts.map
|
package/dist/DuckPond.js
CHANGED
|
@@ -1,2 +1,30 @@
|
|
|
1
|
-
"
|
|
1
|
+
import{loggers as e}from"./utils/logger.js";import{n as t,t as n}from"./LRUCache-Bg_8n4IH.js";import{ErrorCode as r}from"./types.js";import{Errors as i,success as a,toDuckPondError as o}from"./utils/errors.js";import{DuckDBInstance as s}from"@duckdb/node-api";import*as c from"fs";import{Left as l,Option as u}from"functype";import*as d from"path";const f=e.main;var p=class{constructor(e){t(this,`instance`,u.none()),t(this,`cache`,void 0),t(this,`config`,void 0),t(this,`initialized`,!1),this.config={memoryLimit:e.memoryLimit||`4GB`,threads:e.threads||4,tempDir:e.tempDir||`/tmp/duckpond`,maxActiveUsers:e.maxActiveUsers||10,evictionTimeout:e.evictionTimeout||3e5,cacheType:e.cacheType||`disk`,cacheDir:e.cacheDir||`/tmp/duckpond-cache`,strategy:e.strategy||`parquet`,dataDir:e.dataDir,r2:e.r2,s3:e.s3},this.cache=new n({maxSize:this.config.maxActiveUsers,ttl:this.config.evictionTimeout,dispose:(e,t,n)=>{this.cleanupUserResources(e,t,n)}}),f(`DuckPond created with config:`,{memoryLimit:this.config.memoryLimit,threads:this.config.threads,maxActiveUsers:this.config.maxActiveUsers,strategy:this.config.strategy,dataDir:this.config.dataDir,storageMode:this.config.dataDir?`local`:this.config.r2?`r2`:this.config.s3?`s3`:`memory`})}cleanupUserResources(e,t,n){if(f(`Cleaning up user ${t} (reason: ${n})`),this.config.dataDir&&e.instance)try{e.instance.closeSync(),f(`Closed local database instance for user: ${t}`)}catch(e){f(`Error closing instance for user ${t}:`,e)}this.config.strategy===`duckdb`&&!this.config.dataDir&&e.attached&&e.connection.run(`DETACH user_${t}`).then(()=>f(`Detached cloud database for user: ${t}`)).catch(e=>f(`Error detaching database for user ${t}:`,e))}async init(){if(this.initialized)return f(`Already initialized`),a(void 0);f(`Initializing DuckPond...`);try{if(this.config.dataDir&&(await this.ensureDataDir(),f(`Local storage enabled: ${this.config.dataDir}`)),!this.config.dataDir){this.instance=u(await s.create(`:memory:`));let e=await this.setupCloudStorage();if(e.isLeft()){let t=e.fold(e=>e,()=>null);throw Error(t?.message||`Cloud storage setup failed`)}}return this.initialized=!0,f(`DuckPond initialized successfully`),a(void 0)}catch(e){return l(o(e,r.CONNECTION_FAILED))}}async ensureDataDir(){if(this.config.dataDir)try{await c.promises.mkdir(this.config.dataDir,{recursive:!0}),f(`Data directory ready: ${this.config.dataDir}`)}catch(e){throw f(`Failed to create data directory: ${e}`),e}}getUserDbPath(e){if(!this.config.dataDir)throw Error(`dataDir not configured`);let t=e.replace(/[^a-zA-Z0-9_-]/g,`_`);return d.join(this.config.dataDir,`${t}.duckdb`)}async setupCloudStorage(){if(this.instance.isNone())return i.notInitialized();let e=await this.instance.fold(()=>{throw Error(`Unexpected: instance should be Some`)},e=>e).connect();try{if(this.config.r2)f(`Configuring R2 access`),await e.run(`
|
|
2
|
+
CREATE SECRET r2_secret (
|
|
3
|
+
TYPE R2,
|
|
4
|
+
ACCOUNT_ID '${this.config.r2.accountId}',
|
|
5
|
+
ACCESS_KEY_ID '${this.config.r2.accessKeyId}',
|
|
6
|
+
SECRET_ACCESS_KEY '${this.config.r2.secretAccessKey}'
|
|
7
|
+
);
|
|
8
|
+
`);else if(this.config.s3){f(`Configuring S3 access`);let t=this.config.s3.endpoint?`ENDPOINT '${this.config.s3.endpoint}',`:``;await e.run(`
|
|
9
|
+
CREATE SECRET s3_secret (
|
|
10
|
+
TYPE S3,
|
|
11
|
+
REGION '${this.config.s3.region}',
|
|
12
|
+
${t}
|
|
13
|
+
ACCESS_KEY_ID '${this.config.s3.accessKeyId}',
|
|
14
|
+
SECRET_ACCESS_KEY '${this.config.s3.secretAccessKey}'
|
|
15
|
+
);
|
|
16
|
+
`)}return(this.config.r2||this.config.s3)&&(await e.run(`
|
|
17
|
+
INSTALL httpfs;
|
|
18
|
+
LOAD httpfs;
|
|
19
|
+
INSTALL cache_httpfs;
|
|
20
|
+
LOAD cache_httpfs;
|
|
21
|
+
`),await e.run(`
|
|
22
|
+
SET cache_httpfs_type='${this.config.cacheType}';
|
|
23
|
+
`)),await e.run(`
|
|
24
|
+
SET memory_limit='${this.config.memoryLimit}';
|
|
25
|
+
SET threads=${this.config.threads};
|
|
26
|
+
`),f(`Cloud storage configured successfully`),a(void 0)}catch(e){return i.r2ConnectionError(`Failed to setup cloud storage`,e)}}async getUserConnection(e){if(!this.initialized)return i.notInitialized();let t=this.cache.get(e);if(t.isSome())return f(`Using cached connection for user: ${e}`),t.fold(()=>i.userNotFound(e),e=>Promise.resolve(a(e.connection)));if(f(`Loading database for user: ${e}`),this.config.dataDir)return this.createLocalUserConnection(e);if(this.instance.isNone())return i.notInitialized();let n=await this.instance.fold(()=>{throw Error(`Unexpected: instance should be Some`)},e=>e).connect(),r=await this.attachUserDatabase(n,e);return r.isLeft()?r:(this.cache.set(e,{userId:e,connection:n,lastAccess:new Date,attached:!0}),f(`Loaded database for user: ${e}`),a(n))}async createLocalUserConnection(e){try{let t=this.getUserDbPath(e);f(`Creating/opening local database for user ${e}: ${t}`);let n=await s.create(t),r=await n.connect();return await r.run(`
|
|
27
|
+
SET memory_limit='${this.config.memoryLimit}';
|
|
28
|
+
SET threads=${this.config.threads};
|
|
29
|
+
`),this.cache.set(e,{userId:e,connection:r,lastAccess:new Date,attached:!0,instance:n}),f(`Local database ready for user: ${e}`),a(r)}catch(e){return l(o(e,r.CONNECTION_FAILED))}}async attachUserDatabase(e,t){try{let n=this.config.r2?.bucket||this.config.s3?.bucket,r=this.config.r2?`r2`:`s3`;if(this.config.strategy===`duckdb`){let i=`${r}://${n}/users/${t}/database.duckdb`;await e.run(`ATTACH '${i}' AS user_${t} (READ_ONLY);`),f(`Attached .duckdb file for user: ${t}`)}return a(void 0)}catch(e){return i.storageError(`Failed to attach user database`,e)}}async query(e,t){let n=await this.getUserConnection(e);if(n.isLeft())return n;let i=n.fold(()=>{throw Error(`Unexpected: connection should be Right`)},e=>e);try{return a(await(await i.run(t)).getRowObjects())}catch(e){return l(o(e,r.QUERY_EXECUTION_ERROR))}}async execute(e,t){let n=await this.getUserConnection(e);if(n.isLeft())return n;let i=n.fold(()=>{throw Error(`Unexpected: connection should be Right`)},e=>e);try{return await i.run(t),a(void 0)}catch(e){return l(o(e,r.QUERY_EXECUTION_ERROR))}}detachUser(e){return this.cache.has(e)?(f(`Detaching user: ${e}`),this.cache.delete(e),a(void 0)):(f(`User not attached: ${e}`),a(void 0))}isAttached(e){return this.cache.has(e)}async getUserStats(e){let t=this.cache.get(e);return a({userId:e,attached:t.isSome(),lastAccess:t.fold(()=>new Date(0),e=>e.lastAccess),memoryUsage:t.fold(()=>0,e=>e.memoryUsage||0),storageUsage:0,queryCount:0})}listUsers(){let e=this.cache.getStats();return{users:this.cache.keys(),count:e.size,maxActiveUsers:e.maxSize,utilizationPercent:e.utilizationPercent}}close(){return f(`Closing DuckPond...`),this.cache.clear(),this.instance=u.none(),this.initialized=!1,f(`DuckPond closed`),a(void 0)}};export{p as DuckPond};
|
|
2
30
|
//# sourceMappingURL=DuckPond.js.map
|
package/dist/DuckPond.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/Users/jordanburke/IdeaProjects/duckpond/dist/DuckPond.js"],"names":[],"mappings":"AAAA,+HAAkC,+BAA4B,+BAA4B,+BAA4B,+BAA4B,+BAA4B,sCAAsB","file":"/Users/jordanburke/IdeaProjects/duckpond/dist/DuckPond.js"}
|
|
1
|
+
{"version":3,"file":"DuckPond.js","names":[],"sources":["../src/DuckPond.ts"],"sourcesContent":["import { type DuckDBConnection, DuckDBInstance } from \"@duckdb/node-api\"\nimport * as fs from \"fs\"\nimport { Either, Left, Right } from \"functype\"\nimport { Option } from \"functype\"\nimport { Try } from \"functype\"\nimport * as path from \"path\"\n\nimport { LRUCache } from \"./cache/LRUCache\"\nimport type {\n AsyncDuckPondResult,\n CreateUserOptions,\n DuckPondConfig,\n DuckPondResult,\n ListUsersResult,\n ResolvedConfig,\n Schema,\n UserDatabase,\n UserStats,\n} from \"./types\"\nimport { ErrorCode } from \"./types\"\nimport { Errors, success, toDuckPondError } from \"./utils/errors\"\nimport { loggers } from \"./utils/logger\"\n\nconst log = loggers.main\n\n/**\n * DuckPond - Multi-tenant DuckDB manager with R2/S3 storage\n *\n * Manages per-user DuckDB instances with:\n * - LRU caching for active users\n * - R2/S3 object storage integration\n * - Functional error handling with functype Either\n * - Automatic resource cleanup\n *\n * @example\n * ```typescript\n * const pond = new DuckPond({\n * r2: {\n * accountId: 'xxx',\n * accessKeyId: 'yyy',\n * secretAccessKey: 'zzz',\n * bucket: 'my-bucket'\n * }\n * })\n *\n * await pond.init()\n *\n * const result = await pond.query('user123', 'SELECT * FROM orders')\n * result.fold(\n * error => console.error('Query failed:', error),\n * rows => console.log('Results:', rows)\n * )\n * ```\n */\n/** Extended user database with per-user instance for local storage */\ninterface UserDatabaseWithInstance extends UserDatabase {\n instance?: DuckDBInstance\n}\n\nexport class DuckPond {\n private instance: Option<DuckDBInstance> = Option.none()\n private cache: LRUCache<UserDatabaseWithInstance>\n private config: ResolvedConfig\n private initialized = false\n\n constructor(config: DuckPondConfig) {\n // Apply defaults\n this.config = {\n memoryLimit: config.memoryLimit || \"4GB\",\n threads: config.threads || 4,\n tempDir: config.tempDir || \"/tmp/duckpond\",\n maxActiveUsers: config.maxActiveUsers || 10,\n evictionTimeout: config.evictionTimeout || 300000,\n cacheType: config.cacheType || \"disk\",\n cacheDir: config.cacheDir || \"/tmp/duckpond-cache\",\n strategy: config.strategy || \"parquet\",\n dataDir: config.dataDir,\n r2: config.r2,\n s3: config.s3,\n }\n\n // Create cache with TTL-based eviction and automatic cleanup\n this.cache = new LRUCache({\n maxSize: this.config.maxActiveUsers,\n ttl: this.config.evictionTimeout,\n dispose: (userDb, userId, reason) => {\n this.cleanupUserResources(userDb, userId, reason)\n },\n })\n\n log(\"DuckPond created with config:\", {\n memoryLimit: this.config.memoryLimit,\n threads: this.config.threads,\n maxActiveUsers: this.config.maxActiveUsers,\n strategy: this.config.strategy,\n dataDir: this.config.dataDir,\n storageMode: this.config.dataDir ? \"local\" : this.config.r2 ? \"r2\" : this.config.s3 ? \"s3\" : \"memory\",\n })\n }\n\n /**\n * Cleanup resources when a user is evicted from cache\n * Called automatically by LRU cache dispose callback\n */\n private cleanupUserResources(\n userDb: UserDatabaseWithInstance,\n userId: string,\n reason: \"evict\" | \"set\" | \"delete\" | \"stale\",\n ): void {\n log(`Cleaning up user ${userId} (reason: ${reason})`)\n\n // For local storage, close the per-user instance synchronously\n if (this.config.dataDir && userDb.instance) {\n try {\n userDb.instance.closeSync()\n log(`Closed local database instance for user: ${userId}`)\n } catch (error) {\n log(`Error closing instance for user ${userId}:`, error)\n }\n }\n\n // For cloud duckdb strategy, detach the database (fire-and-forget)\n if (this.config.strategy === \"duckdb\" && !this.config.dataDir && userDb.attached) {\n userDb.connection\n .run(`DETACH user_${userId}`)\n .then(() => log(`Detached cloud database for user: ${userId}`))\n .catch((error) => log(`Error detaching database for user ${userId}:`, error))\n }\n }\n\n /**\n * Initialize DuckPond\n * Must be called before any other operations\n */\n async init(): AsyncDuckPondResult<void> {\n if (this.initialized) {\n log(\"Already initialized\")\n return success(undefined)\n }\n\n log(\"Initializing DuckPond...\")\n\n try {\n // Create dataDir if local storage is enabled\n if (this.config.dataDir) {\n await this.ensureDataDir()\n log(`Local storage enabled: ${this.config.dataDir}`)\n }\n\n // For cloud storage or memory mode, create a shared instance\n // For local storage, we create per-user instances in getUserConnection\n if (!this.config.dataDir) {\n const instance = await DuckDBInstance.create(\":memory:\")\n this.instance = Option(instance)\n\n // Setup cloud storage\n const setupResult = await this.setupCloudStorage()\n if (setupResult.isLeft()) {\n const error = setupResult.fold(\n (err) => err,\n () => null,\n )\n throw new Error(error?.message || \"Cloud storage setup failed\")\n }\n }\n\n this.initialized = true\n log(\"DuckPond initialized successfully\")\n return success(undefined)\n } catch (error) {\n return Left(toDuckPondError(error, ErrorCode.CONNECTION_FAILED))\n }\n }\n\n /**\n * Ensure the data directory exists\n */\n private async ensureDataDir(): Promise<void> {\n if (!this.config.dataDir) return\n\n try {\n await fs.promises.mkdir(this.config.dataDir, { recursive: true })\n log(`Data directory ready: ${this.config.dataDir}`)\n } catch (error) {\n log(`Failed to create data directory: ${error}`)\n throw error\n }\n }\n\n /**\n * Get the database file path for a user\n */\n private getUserDbPath(userId: string): string {\n if (!this.config.dataDir) {\n throw new Error(\"dataDir not configured\")\n }\n // Sanitize userId for filesystem safety\n const safeUserId = userId.replace(/[^a-zA-Z0-9_-]/g, \"_\")\n return path.join(this.config.dataDir, `${safeUserId}.duckdb`)\n }\n\n /**\n * Configure R2/S3 access and DuckDB extensions\n */\n private async setupCloudStorage(): AsyncDuckPondResult<void> {\n if (this.instance.isNone()) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return Errors.notInitialized() as any\n }\n\n const instance = this.instance.fold(\n () => {\n throw new Error(\"Unexpected: instance should be Some\")\n },\n (inst) => inst,\n )\n\n const conn = await instance.connect()\n\n try {\n // Create secret for R2 or S3\n if (this.config.r2) {\n log(\"Configuring R2 access\")\n await conn.run(`\n CREATE SECRET r2_secret (\n TYPE R2,\n ACCOUNT_ID '${this.config.r2.accountId}',\n ACCESS_KEY_ID '${this.config.r2.accessKeyId}',\n SECRET_ACCESS_KEY '${this.config.r2.secretAccessKey}'\n );\n `)\n } else if (this.config.s3) {\n log(\"Configuring S3 access\")\n const endpoint = this.config.s3.endpoint ? `ENDPOINT '${this.config.s3.endpoint}',` : \"\"\n await conn.run(`\n CREATE SECRET s3_secret (\n TYPE S3,\n REGION '${this.config.s3.region}',\n ${endpoint}\n ACCESS_KEY_ID '${this.config.s3.accessKeyId}',\n SECRET_ACCESS_KEY '${this.config.s3.secretAccessKey}'\n );\n `)\n }\n\n // Install extensions only if R2/S3 is configured\n if (this.config.r2 || this.config.s3) {\n await conn.run(`\n INSTALL httpfs;\n LOAD httpfs;\n INSTALL cache_httpfs;\n LOAD cache_httpfs;\n `)\n\n // Configure cache\n await conn.run(`\n SET cache_httpfs_type='${this.config.cacheType}';\n `)\n }\n\n // Configure performance settings\n await conn.run(`\n SET memory_limit='${this.config.memoryLimit}';\n SET threads=${this.config.threads};\n `)\n\n log(\"Cloud storage configured successfully\")\n return success(undefined)\n } catch (error) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return Errors.r2ConnectionError(\"Failed to setup cloud storage\", error as Error) as any\n }\n }\n\n /**\n * Get a connection for a user\n * Loads from cache or creates new database\n */\n async getUserConnection(userId: string): AsyncDuckPondResult<DuckDBConnection> {\n if (!this.initialized) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return Errors.notInitialized() as any\n }\n\n // Check cache\n const cached = this.cache.get(userId)\n if (cached.isSome()) {\n log(`Using cached connection for user: ${userId}`)\n return cached.fold(\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (): AsyncDuckPondResult<DuckDBConnection> => Errors.userNotFound(userId) as any,\n (userDb): AsyncDuckPondResult<DuckDBConnection> => Promise.resolve(success(userDb.connection)),\n )\n }\n\n log(`Loading database for user: ${userId}`)\n\n // Note: LRU cache automatically evicts when at capacity via dispose callback\n\n // Local storage mode: create per-user file-based instance\n if (this.config.dataDir) {\n return this.createLocalUserConnection(userId)\n }\n\n // Cloud/memory mode: use shared instance\n if (this.instance.isNone()) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return Errors.notInitialized() as any\n }\n\n const instance = this.instance.fold(\n () => {\n throw new Error(\"Unexpected: instance should be Some\")\n },\n (inst) => inst,\n )\n\n const conn = await instance.connect()\n\n // Attach user's database (strategy-dependent)\n const attachResult = await this.attachUserDatabase(conn, userId)\n if (attachResult.isLeft()) {\n // Note: DuckDB connections are managed by the instance, no need to close\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return attachResult as any\n }\n\n // Add to cache\n this.cache.set(userId, {\n userId,\n connection: conn,\n lastAccess: new Date(),\n attached: true,\n })\n\n log(`Loaded database for user: ${userId}`)\n return success(conn)\n }\n\n /**\n * Create a local file-based database connection for a user\n */\n private async createLocalUserConnection(userId: string): AsyncDuckPondResult<DuckDBConnection> {\n try {\n const dbPath = this.getUserDbPath(userId)\n log(`Creating/opening local database for user ${userId}: ${dbPath}`)\n\n // Create per-user DuckDB instance with file path\n const userInstance = await DuckDBInstance.create(dbPath)\n const conn = await userInstance.connect()\n\n // Configure performance settings\n await conn.run(`\n SET memory_limit='${this.config.memoryLimit}';\n SET threads=${this.config.threads};\n `)\n\n // Add to cache with instance reference\n this.cache.set(userId, {\n userId,\n connection: conn,\n lastAccess: new Date(),\n attached: true,\n instance: userInstance,\n })\n\n log(`Local database ready for user: ${userId}`)\n return success(conn)\n } catch (error) {\n return Left(toDuckPondError(error, ErrorCode.CONNECTION_FAILED))\n }\n }\n\n /**\n * Attach a user's database based on storage strategy\n */\n private async attachUserDatabase(conn: DuckDBConnection, userId: string): AsyncDuckPondResult<void> {\n try {\n const bucket = this.config.r2?.bucket || this.config.s3?.bucket\n const protocol = this.config.r2 ? \"r2\" : \"s3\"\n\n if (this.config.strategy === \"duckdb\") {\n // Attach .duckdb file (read-only)\n const dbPath = `${protocol}://${bucket}/users/${userId}/database.duckdb`\n await conn.run(`ATTACH '${dbPath}' AS user_${userId} (READ_ONLY);`)\n log(`Attached .duckdb file for user: ${userId}`)\n }\n // For parquet strategy, no explicit attach needed\n return success(undefined)\n } catch (error) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return Errors.storageError(\"Failed to attach user database\", error as Error) as any\n }\n }\n\n /**\n * Execute a SQL query for a user\n * Returns Either<Error, results>\n */\n async query<T = unknown>(userId: string, sql: string): AsyncDuckPondResult<T[]> {\n const connResult = await this.getUserConnection(userId)\n\n if (connResult.isLeft()) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return connResult as any\n }\n\n const conn = connResult.fold(\n () => {\n throw new Error(\"Unexpected: connection should be Right\")\n },\n (c) => c,\n )\n\n try {\n const resultObj = await conn.run(sql)\n // DuckDB node-api: use getRowObjects() to get rows as objects\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const rows = (await (resultObj as any).getRowObjects()) as T[]\n\n return success(rows)\n } catch (error) {\n return Left(toDuckPondError(error, ErrorCode.QUERY_EXECUTION_ERROR))\n }\n }\n\n /**\n * Execute SQL without returning results (DDL, DML)\n */\n async execute(userId: string, sql: string): AsyncDuckPondResult<void> {\n const connResult = await this.getUserConnection(userId)\n\n if (connResult.isLeft()) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return connResult as any\n }\n\n const conn = connResult.fold(\n () => {\n throw new Error(\"Unexpected: connection should be Right\")\n },\n (c) => c,\n )\n\n try {\n await conn.run(sql)\n return success(undefined)\n } catch (error) {\n return Left(toDuckPondError(error, ErrorCode.QUERY_EXECUTION_ERROR))\n }\n }\n\n /**\n * Detach a user's database and free resources\n * Cleanup happens automatically via the cache dispose callback\n */\n detachUser(userId: string): DuckPondResult<void> {\n if (!this.cache.has(userId)) {\n log(`User not attached: ${userId}`)\n return success(undefined)\n }\n\n log(`Detaching user: ${userId}`)\n // Delete triggers dispose callback which handles cleanup\n this.cache.delete(userId)\n return success(undefined)\n }\n\n /**\n * Check if a user is currently attached\n */\n isAttached(userId: string): boolean {\n return this.cache.has(userId)\n }\n\n /**\n * Get statistics about a user's database\n */\n async getUserStats(userId: string): AsyncDuckPondResult<UserStats> {\n const cached = this.cache.get(userId)\n\n return success({\n userId,\n attached: cached.isSome(),\n lastAccess: cached.fold(\n () => new Date(0),\n (u) => u.lastAccess,\n ),\n memoryUsage: cached.fold(\n () => 0,\n (u) => u.memoryUsage || 0,\n ),\n storageUsage: 0, // TODO: Calculate from R2\n queryCount: 0, // TODO: Track queries\n })\n }\n\n /**\n * Get list of all currently cached users\n * Returns a List of user IDs and cache statistics\n */\n listUsers(): ListUsersResult {\n const stats = this.cache.getStats()\n const keys = this.cache.keys()\n\n return {\n users: keys, // Already returns List<string>\n count: stats.size,\n maxActiveUsers: stats.maxSize,\n utilizationPercent: stats.utilizationPercent,\n }\n }\n\n /**\n * Close DuckPond and cleanup all resources\n */\n close(): DuckPondResult<void> {\n log(\"Closing DuckPond...\")\n\n // Clear cache - triggers dispose callback for each user\n this.cache.clear()\n\n // Close instance\n this.instance = Option.none()\n this.initialized = false\n log(\"DuckPond closed\")\n return success(undefined)\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{loggers as e}from"./utils/logger.js";import{List as t}from"functype/list";import{Option as n}from"functype/option";import{LRUCache as r}from"lru-cache";function i(e){"@babel/helpers - typeof";return i=typeof Symbol==`function`&&typeof Symbol.iterator==`symbol`?function(e){return typeof e}:function(e){return e&&typeof Symbol==`function`&&e.constructor===Symbol&&e!==Symbol.prototype?`symbol`:typeof e},i(e)}function a(e,t){if(i(e)!=`object`||!e)return e;var n=e[Symbol.toPrimitive];if(n!==void 0){var r=n.call(e,t||`default`);if(i(r)!=`object`)return r;throw TypeError(`@@toPrimitive must return a primitive value.`)}return(t===`string`?String:Number)(e)}function o(e){var t=a(e,`string`);return i(t)==`symbol`?t:t+``}function s(e,t,n){return(t=o(t))in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}const c=e.cache;var l=class{constructor(e){s(this,`cache`,void 0),s(this,`maxSize`,void 0);let t=typeof e==`number`?{maxSize:e}:e;this.maxSize=t.maxSize,this.cache=new r({max:t.maxSize,ttl:t.ttl,updateAgeOnGet:!0,dispose:t.dispose?(e,n,r)=>{c(`Disposing ${n} (reason: ${r})`),t.dispose(e,n,r)}:void 0}),c(`Created LRU cache with maxSize=${t.maxSize}${t.ttl?`, ttl=${t.ttl}ms`:``}`)}get(e){let t=this.cache.get(e);return t===void 0?n.none():(t.lastAccess=new Date,c(`Cache hit: ${e}`),n(t))}set(e,t){t.lastAccess=new Date,this.cache.set(e,t),c(`Cache set: ${e} (size=${this.cache.size})`)}delete(e){let t=this.cache.delete(e);return t&&c(`Cache delete: ${e}`),t}has(e){return this.cache.has(e)}getLRU(){let e=this.cache.rkeys().next();return e.done||e.value===void 0?n.none():n(e.value)}getStale(e){let n=Date.now(),r=[];for(let t of this.cache.keys()){let i=this.cache.peek(t);i&&n-i.lastAccess.getTime()>e&&r.push(t)}return r.length>0&&c(`Found ${r.length} stale items`),t(r)}size(){return this.cache.size}clear(){let e=this.cache.size;this.cache.clear(),c(`Cache cleared (removed ${e} items)`)}values(){return t(Array.from(this.cache.values()))}keys(){return t(Array.from(this.cache.keys()))}purgeStale(){this.cache.purgeStale()}getStats(){let e=n(this.values().toArray().sort((e,t)=>e.lastAccess.getTime()-t.lastAccess.getTime())[0]).map(e=>e.lastAccess);return{size:this.cache.size,maxSize:this.maxSize,utilizationPercent:this.cache.size/this.maxSize*100,oldestAccessTime:e}}};export{s as n,l as t};
|
|
2
|
+
//# sourceMappingURL=LRUCache-Bg_8n4IH.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LRUCache-Bg_8n4IH.js","names":["LRUCache","LRU","staleKeys: string[]"],"sources":["../src/cache/LRUCache.ts"],"sourcesContent":["import { List } from \"functype/list\"\nimport { Option } from \"functype/option\"\nimport { LRUCache as LRU } from \"lru-cache\"\n\nimport type { UserDatabase } from \"../types\"\nimport { loggers } from \"../utils/logger\"\n\nconst log = loggers.cache\n\n/**\n * Cache options for the LRU cache\n */\nexport interface LRUCacheOptions<T extends UserDatabase> {\n /** Maximum number of items in cache */\n maxSize: number\n /** Time-to-live in milliseconds (optional, defaults to no TTL) */\n ttl?: number\n /** Callback when items are evicted/disposed */\n dispose?: (value: T, key: string, reason: \"evict\" | \"set\" | \"delete\" | \"stale\") => void\n}\n\n/**\n * Functype-compatible LRU Cache wrapper\n *\n * Wraps the battle-tested lru-cache package with functype Option<T> and List<T>\n * for type-safe null handling and immutable collections.\n *\n * @example\n * ```typescript\n * const cache = new LRUCache<UserDatabase>({\n * maxSize: 100,\n * ttl: 1000 * 60 * 30, // 30 min\n * dispose: (value, key, reason) => {\n * console.log(`Evicting ${key}: ${reason}`)\n * }\n * })\n *\n * cache.set(\"user1\", userDb)\n * const result = cache.get(\"user1\") // Option<UserDatabase>\n * result.fold(\n * () => console.log(\"Not found\"),\n * (db) => console.log(\"Found:\", db.userId)\n * )\n * ```\n */\nexport class LRUCache<T extends UserDatabase> {\n private cache: LRU<string, T>\n private readonly maxSize: number\n\n constructor(options: number | LRUCacheOptions<T>) {\n const opts = typeof options === \"number\" ? { maxSize: options } : options\n\n this.maxSize = opts.maxSize\n\n this.cache = new LRU<string, T>({\n max: opts.maxSize,\n ttl: opts.ttl,\n updateAgeOnGet: true, // Reset TTL on access\n dispose: opts.dispose\n ? (value, key, reason) => {\n log(`Disposing ${key} (reason: ${reason})`)\n opts.dispose!(value, key, reason as \"evict\" | \"set\" | \"delete\" | \"stale\")\n }\n : undefined,\n })\n\n log(`Created LRU cache with maxSize=${opts.maxSize}${opts.ttl ? `, ttl=${opts.ttl}ms` : \"\"}`)\n }\n\n /**\n * Get a value from the cache\n * Returns Option.Some(value) if found, Option.None otherwise\n */\n get(key: string): Option<T> {\n const value = this.cache.get(key)\n if (value !== undefined) {\n // Update lastAccess for stats tracking\n value.lastAccess = new Date()\n log(`Cache hit: ${key}`)\n return Option(value)\n }\n return Option.none()\n }\n\n /**\n * Set a value in the cache\n * Automatically evicts LRU item if at capacity\n */\n set(key: string, value: T): void {\n value.lastAccess = new Date()\n this.cache.set(key, value)\n log(`Cache set: ${key} (size=${this.cache.size})`)\n }\n\n /**\n * Remove a value from the cache\n */\n delete(key: string): boolean {\n const deleted = this.cache.delete(key)\n if (deleted) {\n log(`Cache delete: ${key}`)\n }\n return deleted\n }\n\n /**\n * Check if a key exists in the cache (without updating recency)\n */\n has(key: string): boolean {\n return this.cache.has(key)\n }\n\n /**\n * Get the least recently used key\n * Returns Option.Some(key) if cache not empty, Option.None otherwise\n *\n * Note: lru-cache maintains LRU order, so we iterate from oldest to newest\n */\n getLRU(): Option<string> {\n // lru-cache keys() iterates from most recent to least recent by default\n // Use rkeys() to get reverse (LRU first) order\n const result = this.cache.rkeys().next()\n if (result.done || result.value === undefined) {\n return Option.none()\n }\n return Option(result.value)\n }\n\n /**\n * Get all keys for items older than the timeout\n * Returns a List of stale keys (functype immutable list)\n *\n * Note: If using TTL, stale items are automatically handled by the cache.\n * This method is for manual staleness checks based on lastAccess.\n */\n getStale(timeoutMs: number): List<string> {\n const now = Date.now()\n const staleKeys: string[] = []\n\n for (const key of this.cache.keys()) {\n const value = this.cache.peek(key) // peek doesn't update recency\n if (value && now - value.lastAccess.getTime() > timeoutMs) {\n staleKeys.push(key)\n }\n }\n\n if (staleKeys.length > 0) {\n log(`Found ${staleKeys.length} stale items`)\n }\n\n return List(staleKeys)\n }\n\n /**\n * Get the current size of the cache\n */\n size(): number {\n return this.cache.size\n }\n\n /**\n * Clear all items from the cache\n * Note: This triggers dispose callbacks for each item\n */\n clear(): void {\n const size = this.cache.size\n this.cache.clear()\n log(`Cache cleared (removed ${size} items)`)\n }\n\n /**\n * Get all values as a List (functype immutable list)\n */\n values(): List<T> {\n return List(Array.from(this.cache.values()))\n }\n\n /**\n * Get all keys as a List\n */\n keys(): List<string> {\n return List(Array.from(this.cache.keys()))\n }\n\n /**\n * Purge stale items (TTL expired)\n * Call this to force cleanup of expired items\n */\n purgeStale(): void {\n this.cache.purgeStale()\n }\n\n /**\n * Get cache statistics\n */\n getStats(): {\n size: number\n maxSize: number\n utilizationPercent: number\n oldestAccessTime: Option<Date>\n } {\n const values = this.values().toArray()\n const sorted = values.sort((a, b) => a.lastAccess.getTime() - b.lastAccess.getTime())\n const oldestAccessTime = Option(sorted[0]).map((item) => item.lastAccess)\n\n return {\n size: this.cache.size,\n maxSize: this.maxSize,\n utilizationPercent: (this.cache.size / this.maxSize) * 100,\n oldestAccessTime,\n }\n }\n}\n"],"mappings":"m1BAOA,MAAM,EAAM,EAAQ,MAsCpB,IAAaA,EAAb,KAA8C,CAI5C,YAAY,EAAsC,QAH1C,QAAA,IAAA,GAAA,QACS,UAAA,IAAA,GAAA,CAGf,IAAM,EAAO,OAAO,GAAY,SAAW,CAAE,QAAS,EAAS,CAAG,EAElE,KAAK,QAAU,EAAK,QAEpB,KAAK,MAAQ,IAAIC,EAAe,CAC9B,IAAK,EAAK,QACV,IAAK,EAAK,IACV,eAAgB,GAChB,QAAS,EAAK,SACT,EAAO,EAAK,IAAW,CACtB,EAAI,aAAa,EAAI,YAAY,EAAO,GAAG,CAC3C,EAAK,QAAS,EAAO,EAAK,EAA+C,EAE3E,IAAA,GACL,CAAC,CAEF,EAAI,kCAAkC,EAAK,UAAU,EAAK,IAAM,SAAS,EAAK,IAAI,IAAM,KAAK,CAO/F,IAAI,EAAwB,CAC1B,IAAM,EAAQ,KAAK,MAAM,IAAI,EAAI,CAOjC,OANI,IAAU,IAAA,GAMP,EAAO,MAAM,EAJlB,EAAM,WAAa,IAAI,KACvB,EAAI,cAAc,IAAM,CACjB,EAAO,EAAM,EASxB,IAAI,EAAa,EAAgB,CAC/B,EAAM,WAAa,IAAI,KACvB,KAAK,MAAM,IAAI,EAAK,EAAM,CAC1B,EAAI,cAAc,EAAI,SAAS,KAAK,MAAM,KAAK,GAAG,CAMpD,OAAO,EAAsB,CAC3B,IAAM,EAAU,KAAK,MAAM,OAAO,EAAI,CAItC,OAHI,GACF,EAAI,iBAAiB,IAAM,CAEtB,EAMT,IAAI,EAAsB,CACxB,OAAO,KAAK,MAAM,IAAI,EAAI,CAS5B,QAAyB,CAGvB,IAAM,EAAS,KAAK,MAAM,OAAO,CAAC,MAAM,CAIxC,OAHI,EAAO,MAAQ,EAAO,QAAU,IAAA,GAC3B,EAAO,MAAM,CAEf,EAAO,EAAO,MAAM,CAU7B,SAAS,EAAiC,CACxC,IAAM,EAAM,KAAK,KAAK,CAChBC,EAAsB,EAAE,CAE9B,IAAK,IAAM,KAAO,KAAK,MAAM,MAAM,CAAE,CACnC,IAAM,EAAQ,KAAK,MAAM,KAAK,EAAI,CAC9B,GAAS,EAAM,EAAM,WAAW,SAAS,CAAG,GAC9C,EAAU,KAAK,EAAI,CAQvB,OAJI,EAAU,OAAS,GACrB,EAAI,SAAS,EAAU,OAAO,cAAc,CAGvC,EAAK,EAAU,CAMxB,MAAe,CACb,OAAO,KAAK,MAAM,KAOpB,OAAc,CACZ,IAAM,EAAO,KAAK,MAAM,KACxB,KAAK,MAAM,OAAO,CAClB,EAAI,0BAA0B,EAAK,SAAS,CAM9C,QAAkB,CAChB,OAAO,EAAK,MAAM,KAAK,KAAK,MAAM,QAAQ,CAAC,CAAC,CAM9C,MAAqB,CACnB,OAAO,EAAK,MAAM,KAAK,KAAK,MAAM,MAAM,CAAC,CAAC,CAO5C,YAAmB,CACjB,KAAK,MAAM,YAAY,CAMzB,UAKE,CAGA,IAAM,EAAmB,EAFV,KAAK,QAAQ,CAAC,SAAS,CAChB,MAAM,EAAG,IAAM,EAAE,WAAW,SAAS,CAAG,EAAE,WAAW,SAAS,CAAC,CAC9C,GAAG,CAAC,IAAK,GAAS,EAAK,WAAW,CAEzE,MAAO,CACL,KAAM,KAAK,MAAM,KACjB,QAAS,KAAK,QACd,mBAAqB,KAAK,MAAM,KAAO,KAAK,QAAW,IACvD,mBACD"}
|
package/dist/cache/LRUCache.d.ts
CHANGED
|
@@ -1,75 +1,113 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import '@duckdb/node-api';
|
|
5
|
-
import 'functype/either';
|
|
1
|
+
import { UserDatabase } from "../types.js";
|
|
2
|
+
import { List } from "functype/list";
|
|
3
|
+
import { Option } from "functype/option";
|
|
6
4
|
|
|
5
|
+
//#region src/cache/LRUCache.d.ts
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Cache options for the LRU cache
|
|
9
|
+
*/
|
|
10
|
+
interface LRUCacheOptions<T extends UserDatabase> {
|
|
11
|
+
/** Maximum number of items in cache */
|
|
12
|
+
maxSize: number;
|
|
13
|
+
/** Time-to-live in milliseconds (optional, defaults to no TTL) */
|
|
14
|
+
ttl?: number;
|
|
15
|
+
/** Callback when items are evicted/disposed */
|
|
16
|
+
dispose?: (value: T, key: string, reason: "evict" | "set" | "delete" | "stale") => void;
|
|
17
|
+
}
|
|
7
18
|
/**
|
|
8
|
-
* LRU Cache
|
|
19
|
+
* Functype-compatible LRU Cache wrapper
|
|
9
20
|
*
|
|
10
|
-
*
|
|
21
|
+
* Wraps the battle-tested lru-cache package with functype Option<T> and List<T>
|
|
22
|
+
* for type-safe null handling and immutable collections.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```typescript
|
|
26
|
+
* const cache = new LRUCache<UserDatabase>({
|
|
27
|
+
* maxSize: 100,
|
|
28
|
+
* ttl: 1000 * 60 * 30, // 30 min
|
|
29
|
+
* dispose: (value, key, reason) => {
|
|
30
|
+
* console.log(`Evicting ${key}: ${reason}`)
|
|
31
|
+
* }
|
|
32
|
+
* })
|
|
33
|
+
*
|
|
34
|
+
* cache.set("user1", userDb)
|
|
35
|
+
* const result = cache.get("user1") // Option<UserDatabase>
|
|
36
|
+
* result.fold(
|
|
37
|
+
* () => console.log("Not found"),
|
|
38
|
+
* (db) => console.log("Found:", db.userId)
|
|
39
|
+
* )
|
|
40
|
+
* ```
|
|
11
41
|
*/
|
|
12
42
|
declare class LRUCache<T extends UserDatabase> {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
43
|
+
private cache;
|
|
44
|
+
private readonly maxSize;
|
|
45
|
+
constructor(options: number | LRUCacheOptions<T>);
|
|
46
|
+
/**
|
|
47
|
+
* Get a value from the cache
|
|
48
|
+
* Returns Option.Some(value) if found, Option.None otherwise
|
|
49
|
+
*/
|
|
50
|
+
get(key: string): Option<T>;
|
|
51
|
+
/**
|
|
52
|
+
* Set a value in the cache
|
|
53
|
+
* Automatically evicts LRU item if at capacity
|
|
54
|
+
*/
|
|
55
|
+
set(key: string, value: T): void;
|
|
56
|
+
/**
|
|
57
|
+
* Remove a value from the cache
|
|
58
|
+
*/
|
|
59
|
+
delete(key: string): boolean;
|
|
60
|
+
/**
|
|
61
|
+
* Check if a key exists in the cache (without updating recency)
|
|
62
|
+
*/
|
|
63
|
+
has(key: string): boolean;
|
|
64
|
+
/**
|
|
65
|
+
* Get the least recently used key
|
|
66
|
+
* Returns Option.Some(key) if cache not empty, Option.None otherwise
|
|
67
|
+
*
|
|
68
|
+
* Note: lru-cache maintains LRU order, so we iterate from oldest to newest
|
|
69
|
+
*/
|
|
70
|
+
getLRU(): Option<string>;
|
|
71
|
+
/**
|
|
72
|
+
* Get all keys for items older than the timeout
|
|
73
|
+
* Returns a List of stale keys (functype immutable list)
|
|
74
|
+
*
|
|
75
|
+
* Note: If using TTL, stale items are automatically handled by the cache.
|
|
76
|
+
* This method is for manual staleness checks based on lastAccess.
|
|
77
|
+
*/
|
|
78
|
+
getStale(timeoutMs: number): List<string>;
|
|
79
|
+
/**
|
|
80
|
+
* Get the current size of the cache
|
|
81
|
+
*/
|
|
82
|
+
size(): number;
|
|
83
|
+
/**
|
|
84
|
+
* Clear all items from the cache
|
|
85
|
+
* Note: This triggers dispose callbacks for each item
|
|
86
|
+
*/
|
|
87
|
+
clear(): void;
|
|
88
|
+
/**
|
|
89
|
+
* Get all values as a List (functype immutable list)
|
|
90
|
+
*/
|
|
91
|
+
values(): List<T>;
|
|
92
|
+
/**
|
|
93
|
+
* Get all keys as a List
|
|
94
|
+
*/
|
|
95
|
+
keys(): List<string>;
|
|
96
|
+
/**
|
|
97
|
+
* Purge stale items (TTL expired)
|
|
98
|
+
* Call this to force cleanup of expired items
|
|
99
|
+
*/
|
|
100
|
+
purgeStale(): void;
|
|
101
|
+
/**
|
|
102
|
+
* Get cache statistics
|
|
103
|
+
*/
|
|
104
|
+
getStats(): {
|
|
105
|
+
size: number;
|
|
106
|
+
maxSize: number;
|
|
107
|
+
utilizationPercent: number;
|
|
108
|
+
oldestAccessTime: Option<Date>;
|
|
109
|
+
};
|
|
73
110
|
}
|
|
74
|
-
|
|
75
|
-
export { LRUCache };
|
|
111
|
+
//#endregion
|
|
112
|
+
export { LRUCache, LRUCacheOptions };
|
|
113
|
+
//# sourceMappingURL=LRUCache.d.ts.map
|
package/dist/cache/LRUCache.js
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
"
|
|
2
|
-
//# sourceMappingURL=LRUCache.js.map
|
|
1
|
+
import"../utils/logger.js";import{t as e}from"../LRUCache-Bg_8n4IH.js";export{e as LRUCache};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
import 'functype';
|
|
12
|
-
import 'debug';
|
|
1
|
+
import { AsyncDuckPondResult, ColumnSchema, CreateUserOptions, DuckPondConfig, DuckPondError, DuckPondEvent, DuckPondMetrics, DuckPondResult, ErrorCode, FileInfo, ListUsersResult, QueryResult, ResolvedConfig, Schema, StorageStats, TableSchema, UserDatabase, UserStats } from "./types.js";
|
|
2
|
+
import { DuckPond } from "./DuckPond.js";
|
|
3
|
+
import { LRUCache, LRUCacheOptions } from "./cache/LRUCache.js";
|
|
4
|
+
import { Errors, createError, formatError, success, toDuckPondError } from "./utils/errors.js";
|
|
5
|
+
import { createLogger, loggers } from "./utils/logger.js";
|
|
6
|
+
import { List } from "functype/list";
|
|
7
|
+
import { Option } from "functype/option";
|
|
8
|
+
import { Either, Left, Right } from "functype/either";
|
|
9
|
+
import { Try } from "functype/try";
|
|
10
|
+
export { type AsyncDuckPondResult, type ColumnSchema, type CreateUserOptions, DuckPond, type DuckPondConfig, type DuckPondError, type DuckPondEvent, type DuckPondMetrics, type DuckPondResult, Either, ErrorCode, Errors, type FileInfo, LRUCache, type LRUCacheOptions, Left, List, type ListUsersResult, Option, type QueryResult, type ResolvedConfig, Right, type Schema, type StorageStats, type TableSchema, Try, type UserDatabase, type UserStats, createError, createLogger, formatError, loggers, success, toDuckPondError };
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
2
|
-
//# sourceMappingURL=index.js.map
|
|
1
|
+
import{createLogger as e,loggers as t}from"./utils/logger.js";import{t as n}from"./LRUCache-Bg_8n4IH.js";import{ErrorCode as r}from"./types.js";import{Errors as i,createError as a,formatError as o,success as s,toDuckPondError as c}from"./utils/errors.js";import{DuckPond as l}from"./DuckPond.js";import{List as u}from"functype/list";import{Option as d}from"functype/option";import{Either as f,Left as p,Right as m}from"functype/either";import{Try as h}from"functype/try";export{l as DuckPond,f as Either,r as ErrorCode,i as Errors,n as LRUCache,p as Left,u as List,d as Option,m as Right,h as Try,a as createError,e as createLogger,o as formatError,t as loggers,s as success,c as toDuckPondError};
|