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.
Files changed (63) hide show
  1. package/dist/DuckPond.d.ts +76 -96
  2. package/dist/DuckPond.js +29 -1
  3. package/dist/DuckPond.js.map +1 -1
  4. package/dist/LRUCache-Bg_8n4IH.js +2 -0
  5. package/dist/LRUCache-Bg_8n4IH.js.map +1 -0
  6. package/dist/cache/LRUCache.d.ts +107 -69
  7. package/dist/cache/LRUCache.js +1 -2
  8. package/dist/index.d.ts +10 -12
  9. package/dist/index.js +1 -2
  10. package/dist/types.d.ts +118 -113
  11. package/dist/types.js +1 -1
  12. package/dist/types.js.map +1 -1
  13. package/dist/utils/errors.d.ts +18 -18
  14. package/dist/utils/errors.js +2 -1
  15. package/dist/utils/errors.js.map +1 -1
  16. package/dist/utils/logger.d.ts +11 -8
  17. package/dist/utils/logger.js +1 -1
  18. package/dist/utils/logger.js.map +1 -1
  19. package/package.json +28 -43
  20. package/dist/DuckPond.d.mts +0 -100
  21. package/dist/DuckPond.mjs +0 -2
  22. package/dist/DuckPond.mjs.map +0 -1
  23. package/dist/cache/LRUCache.d.mts +0 -75
  24. package/dist/cache/LRUCache.js.map +0 -1
  25. package/dist/cache/LRUCache.mjs +0 -2
  26. package/dist/cache/LRUCache.mjs.map +0 -1
  27. package/dist/chunk-24M54WUC.mjs +0 -2
  28. package/dist/chunk-24M54WUC.mjs.map +0 -1
  29. package/dist/chunk-2PPSIE57.js +0 -2
  30. package/dist/chunk-2PPSIE57.js.map +0 -1
  31. package/dist/chunk-5XGN7UAV.js +0 -2
  32. package/dist/chunk-5XGN7UAV.js.map +0 -1
  33. package/dist/chunk-A5IXJPPT.mjs +0 -2
  34. package/dist/chunk-A5IXJPPT.mjs.map +0 -1
  35. package/dist/chunk-J2OQ62DV.js +0 -2
  36. package/dist/chunk-J2OQ62DV.js.map +0 -1
  37. package/dist/chunk-MLZHFU2B.js +0 -27
  38. package/dist/chunk-MLZHFU2B.js.map +0 -1
  39. package/dist/chunk-MZTKR3LR.js +0 -3
  40. package/dist/chunk-MZTKR3LR.js.map +0 -1
  41. package/dist/chunk-PAHYBGNR.mjs +0 -27
  42. package/dist/chunk-PAHYBGNR.mjs.map +0 -1
  43. package/dist/chunk-PCQEPXO3.mjs +0 -3
  44. package/dist/chunk-PCQEPXO3.mjs.map +0 -1
  45. package/dist/chunk-Q6UFPTQC.js +0 -2
  46. package/dist/chunk-Q6UFPTQC.js.map +0 -1
  47. package/dist/chunk-SZJXSB7U.mjs +0 -2
  48. package/dist/chunk-SZJXSB7U.mjs.map +0 -1
  49. package/dist/chunk-V57JCP3U.mjs +0 -2
  50. package/dist/chunk-V57JCP3U.mjs.map +0 -1
  51. package/dist/index.d.mts +0 -12
  52. package/dist/index.js.map +0 -1
  53. package/dist/index.mjs +0 -2
  54. package/dist/index.mjs.map +0 -1
  55. package/dist/types.d.mts +0 -192
  56. package/dist/types.mjs +0 -2
  57. package/dist/types.mjs.map +0 -1
  58. package/dist/utils/errors.d.mts +0 -41
  59. package/dist/utils/errors.mjs +0 -2
  60. package/dist/utils/errors.mjs.map +0 -1
  61. package/dist/utils/logger.d.mts +0 -25
  62. package/dist/utils/logger.mjs +0 -2
  63. package/dist/utils/logger.mjs.map +0 -1
@@ -1,100 +1,80 @@
1
- import { DuckDBConnection } from '@duckdb/node-api';
2
- import { DuckPondConfig, AsyncDuckPondResult, UserStats, ListUsersResult } from './types.js';
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
- private instance;
37
- private cache;
38
- private config;
39
- private evictionTimer;
40
- private initialized;
41
- constructor(config: DuckPondConfig);
42
- /**
43
- * Initialize DuckPond
44
- * Must be called before any other operations
45
- */
46
- init(): AsyncDuckPondResult<void>;
47
- /**
48
- * Configure R2/S3 access and DuckDB extensions
49
- */
50
- private setupCloudStorage;
51
- /**
52
- * Get a connection for a user
53
- * Loads from cache or attaches new database
54
- */
55
- getUserConnection(userId: string): AsyncDuckPondResult<DuckDBConnection>;
56
- /**
57
- * Attach a user's database based on storage strategy
58
- */
59
- private attachUserDatabase;
60
- /**
61
- * Execute a SQL query for a user
62
- * Returns Either<Error, results>
63
- */
64
- query<T = unknown>(userId: string, sql: string): AsyncDuckPondResult<T[]>;
65
- /**
66
- * Execute SQL without returning results (DDL, DML)
67
- */
68
- execute(userId: string, sql: string): AsyncDuckPondResult<void>;
69
- /**
70
- * Detach a user's database and free resources
71
- */
72
- detachUser(userId: string): AsyncDuckPondResult<void>;
73
- /**
74
- * Evict the least recently used user
75
- */
76
- private evictLRU;
77
- /**
78
- * Start background timer to evict idle users
79
- */
80
- private startEvictionTimer;
81
- /**
82
- * Check if a user is currently attached
83
- */
84
- isAttached(userId: string): boolean;
85
- /**
86
- * Get statistics about a user's database
87
- */
88
- getUserStats(userId: string): AsyncDuckPondResult<UserStats>;
89
- /**
90
- * Get list of all currently cached users
91
- * Returns a List of user IDs and cache statistics
92
- */
93
- listUsers(): ListUsersResult;
94
- /**
95
- * Close DuckPond and cleanup all resources
96
- */
97
- close(): AsyncDuckPondResult<void>;
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
- "use strict";Object.defineProperty(exports, "__esModule", {value: true});var _chunkMLZHFU2Bjs = require('./chunk-MLZHFU2B.js');require('./chunk-J2OQ62DV.js');require('./chunk-MZTKR3LR.js');require('./chunk-2PPSIE57.js');require('./chunk-Q6UFPTQC.js');require('./chunk-5XGN7UAV.js');exports.DuckPond = _chunkMLZHFU2Bjs.a;
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
@@ -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":"4VAuBA,MAAM,EAAM,EAAQ,KAoCpB,IAAa,EAAb,KAAsB,CAMpB,YAAY,EAAwB,QAL5B,WAAmC,EAAO,MAAM,CAAA,QAChD,QAAA,IAAA,GAAA,QACA,SAAA,IAAA,GAAA,QACA,cAAc,GAAA,CAIpB,KAAK,OAAS,CACZ,YAAa,EAAO,aAAe,MACnC,QAAS,EAAO,SAAW,EAC3B,QAAS,EAAO,SAAW,gBAC3B,eAAgB,EAAO,gBAAkB,GACzC,gBAAiB,EAAO,iBAAmB,IAC3C,UAAW,EAAO,WAAa,OAC/B,SAAU,EAAO,UAAY,sBAC7B,SAAU,EAAO,UAAY,UAC7B,QAAS,EAAO,QAChB,GAAI,EAAO,GACX,GAAI,EAAO,GACZ,CAGD,KAAK,MAAQ,IAAI,EAAS,CACxB,QAAS,KAAK,OAAO,eACrB,IAAK,KAAK,OAAO,gBACjB,SAAU,EAAQ,EAAQ,IAAW,CACnC,KAAK,qBAAqB,EAAQ,EAAQ,EAAO,EAEpD,CAAC,CAEF,EAAI,gCAAiC,CACnC,YAAa,KAAK,OAAO,YACzB,QAAS,KAAK,OAAO,QACrB,eAAgB,KAAK,OAAO,eAC5B,SAAU,KAAK,OAAO,SACtB,QAAS,KAAK,OAAO,QACrB,YAAa,KAAK,OAAO,QAAU,QAAU,KAAK,OAAO,GAAK,KAAO,KAAK,OAAO,GAAK,KAAO,SAC9F,CAAC,CAOJ,qBACE,EACA,EACA,EACM,CAIN,GAHA,EAAI,oBAAoB,EAAO,YAAY,EAAO,GAAG,CAGjD,KAAK,OAAO,SAAW,EAAO,SAChC,GAAI,CACF,EAAO,SAAS,WAAW,CAC3B,EAAI,4CAA4C,IAAS,OAClD,EAAO,CACd,EAAI,mCAAmC,EAAO,GAAI,EAAM,CAKxD,KAAK,OAAO,WAAa,UAAY,CAAC,KAAK,OAAO,SAAW,EAAO,UACtE,EAAO,WACJ,IAAI,eAAe,IAAS,CAC5B,SAAW,EAAI,qCAAqC,IAAS,CAAC,CAC9D,MAAO,GAAU,EAAI,qCAAqC,EAAO,GAAI,EAAM,CAAC,CAQnF,MAAM,MAAkC,CACtC,GAAI,KAAK,YAEP,OADA,EAAI,sBAAsB,CACnB,EAAQ,IAAA,GAAU,CAG3B,EAAI,2BAA2B,CAE/B,GAAI,CASF,GAPI,KAAK,OAAO,UACd,MAAM,KAAK,eAAe,CAC1B,EAAI,0BAA0B,KAAK,OAAO,UAAU,EAKlD,CAAC,KAAK,OAAO,QAAS,CAExB,KAAK,SAAW,EADC,MAAM,EAAe,OAAO,WAAW,CACxB,CAGhC,IAAM,EAAc,MAAM,KAAK,mBAAmB,CAClD,GAAI,EAAY,QAAQ,CAAE,CACxB,IAAM,EAAQ,EAAY,KACvB,GAAQ,MACH,KACP,CACD,MAAU,MAAM,GAAO,SAAW,6BAA6B,EAMnE,MAFA,MAAK,YAAc,GACnB,EAAI,oCAAoC,CACjC,EAAQ,IAAA,GAAU,OAClB,EAAO,CACd,OAAO,EAAK,EAAgB,EAAO,EAAU,kBAAkB,CAAC,EAOpE,MAAc,eAA+B,CACtC,QAAK,OAAO,QAEjB,GAAI,CACF,MAAM,EAAG,SAAS,MAAM,KAAK,OAAO,QAAS,CAAE,UAAW,GAAM,CAAC,CACjE,EAAI,yBAAyB,KAAK,OAAO,UAAU,OAC5C,EAAO,CAEd,MADA,EAAI,oCAAoC,IAAQ,CAC1C,GAOV,cAAsB,EAAwB,CAC5C,GAAI,CAAC,KAAK,OAAO,QACf,MAAU,MAAM,yBAAyB,CAG3C,IAAM,EAAa,EAAO,QAAQ,kBAAmB,IAAI,CACzD,OAAO,EAAK,KAAK,KAAK,OAAO,QAAS,GAAG,EAAW,SAAS,CAM/D,MAAc,mBAA+C,CAC3D,GAAI,KAAK,SAAS,QAAQ,CAExB,OAAO,EAAO,gBAAgB,CAUhC,IAAM,EAAO,MAPI,KAAK,SAAS,SACvB,CACJ,MAAU,MAAM,sCAAsC,EAEvD,GAAS,EACX,CAE2B,SAAS,CAErC,GAAI,CAEF,GAAI,KAAK,OAAO,GACd,EAAI,wBAAwB,CAC5B,MAAM,EAAK,IAAI;;;0BAGG,KAAK,OAAO,GAAG,UAAU;6BACtB,KAAK,OAAO,GAAG,YAAY;iCACvB,KAAK,OAAO,GAAG,gBAAgB;;UAEtD,SACO,KAAK,OAAO,GAAI,CACzB,EAAI,wBAAwB,CAC5B,IAAM,EAAW,KAAK,OAAO,GAAG,SAAW,aAAa,KAAK,OAAO,GAAG,SAAS,IAAM,GACtF,MAAM,EAAK,IAAI;;;sBAGD,KAAK,OAAO,GAAG,OAAO;cAC9B,EAAS;6BACM,KAAK,OAAO,GAAG,YAAY;iCACvB,KAAK,OAAO,GAAG,gBAAgB;;UAEtD,CAyBJ,OArBI,KAAK,OAAO,IAAM,KAAK,OAAO,MAChC,MAAM,EAAK,IAAI;;;;;UAKb,CAGF,MAAM,EAAK,IAAI;mCACY,KAAK,OAAO,UAAU;UAC/C,EAIJ,MAAM,EAAK,IAAI;4BACO,KAAK,OAAO,YAAY;sBAC9B,KAAK,OAAO,QAAQ;QAClC,CAEF,EAAI,wCAAwC,CACrC,EAAQ,IAAA,GAAU,OAClB,EAAO,CAEd,OAAO,EAAO,kBAAkB,gCAAiC,EAAe,EAQpF,MAAM,kBAAkB,EAAuD,CAC7E,GAAI,CAAC,KAAK,YAER,OAAO,EAAO,gBAAgB,CAIhC,IAAM,EAAS,KAAK,MAAM,IAAI,EAAO,CACrC,GAAI,EAAO,QAAQ,CAEjB,OADA,EAAI,qCAAqC,IAAS,CAC3C,EAAO,SAEiC,EAAO,aAAa,EAAO,CACvE,GAAkD,QAAQ,QAAQ,EAAQ,EAAO,WAAW,CAAC,CAC/F,CAQH,GALA,EAAI,8BAA8B,IAAS,CAKvC,KAAK,OAAO,QACd,OAAO,KAAK,0BAA0B,EAAO,CAI/C,GAAI,KAAK,SAAS,QAAQ,CAExB,OAAO,EAAO,gBAAgB,CAUhC,IAAM,EAAO,MAPI,KAAK,SAAS,SACvB,CACJ,MAAU,MAAM,sCAAsC,EAEvD,GAAS,EACX,CAE2B,SAAS,CAG/B,EAAe,MAAM,KAAK,mBAAmB,EAAM,EAAO,CAgBhE,OAfI,EAAa,QAAQ,CAGhB,GAIT,KAAK,MAAM,IAAI,EAAQ,CACrB,SACA,WAAY,EACZ,WAAY,IAAI,KAChB,SAAU,GACX,CAAC,CAEF,EAAI,6BAA6B,IAAS,CACnC,EAAQ,EAAK,EAMtB,MAAc,0BAA0B,EAAuD,CAC7F,GAAI,CACF,IAAM,EAAS,KAAK,cAAc,EAAO,CACzC,EAAI,4CAA4C,EAAO,IAAI,IAAS,CAGpE,IAAM,EAAe,MAAM,EAAe,OAAO,EAAO,CAClD,EAAO,MAAM,EAAa,SAAS,CAkBzC,OAfA,MAAM,EAAK,IAAI;4BACO,KAAK,OAAO,YAAY;sBAC9B,KAAK,OAAO,QAAQ;QAClC,CAGF,KAAK,MAAM,IAAI,EAAQ,CACrB,SACA,WAAY,EACZ,WAAY,IAAI,KAChB,SAAU,GACV,SAAU,EACX,CAAC,CAEF,EAAI,kCAAkC,IAAS,CACxC,EAAQ,EAAK,OACb,EAAO,CACd,OAAO,EAAK,EAAgB,EAAO,EAAU,kBAAkB,CAAC,EAOpE,MAAc,mBAAmB,EAAwB,EAA2C,CAClG,GAAI,CACF,IAAM,EAAS,KAAK,OAAO,IAAI,QAAU,KAAK,OAAO,IAAI,OACnD,EAAW,KAAK,OAAO,GAAK,KAAO,KAEzC,GAAI,KAAK,OAAO,WAAa,SAAU,CAErC,IAAM,EAAS,GAAG,EAAS,KAAK,EAAO,SAAS,EAAO,kBACvD,MAAM,EAAK,IAAI,WAAW,EAAO,YAAY,EAAO,eAAe,CACnE,EAAI,mCAAmC,IAAS,CAGlD,OAAO,EAAQ,IAAA,GAAU,OAClB,EAAO,CAEd,OAAO,EAAO,aAAa,iCAAkC,EAAe,EAQhF,MAAM,MAAmB,EAAgB,EAAuC,CAC9E,IAAM,EAAa,MAAM,KAAK,kBAAkB,EAAO,CAEvD,GAAI,EAAW,QAAQ,CAErB,OAAO,EAGT,IAAM,EAAO,EAAW,SAChB,CACJ,MAAU,MAAM,yCAAyC,EAE1D,GAAM,EACR,CAED,GAAI,CAMF,OAAO,EAFO,MAHI,MAAM,EAAK,IAAI,EAAI,EAGE,eAAe,CAElC,OACb,EAAO,CACd,OAAO,EAAK,EAAgB,EAAO,EAAU,sBAAsB,CAAC,EAOxE,MAAM,QAAQ,EAAgB,EAAwC,CACpE,IAAM,EAAa,MAAM,KAAK,kBAAkB,EAAO,CAEvD,GAAI,EAAW,QAAQ,CAErB,OAAO,EAGT,IAAM,EAAO,EAAW,SAChB,CACJ,MAAU,MAAM,yCAAyC,EAE1D,GAAM,EACR,CAED,GAAI,CAEF,OADA,MAAM,EAAK,IAAI,EAAI,CACZ,EAAQ,IAAA,GAAU,OAClB,EAAO,CACd,OAAO,EAAK,EAAgB,EAAO,EAAU,sBAAsB,CAAC,EAQxE,WAAW,EAAsC,CAS/C,OARK,KAAK,MAAM,IAAI,EAAO,EAK3B,EAAI,mBAAmB,IAAS,CAEhC,KAAK,MAAM,OAAO,EAAO,CAClB,EAAQ,IAAA,GAAU,GAPvB,EAAI,sBAAsB,IAAS,CAC5B,EAAQ,IAAA,GAAU,EAY7B,WAAW,EAAyB,CAClC,OAAO,KAAK,MAAM,IAAI,EAAO,CAM/B,MAAM,aAAa,EAAgD,CACjE,IAAM,EAAS,KAAK,MAAM,IAAI,EAAO,CAErC,OAAO,EAAQ,CACb,SACA,SAAU,EAAO,QAAQ,CACzB,WAAY,EAAO,SACX,IAAI,KAAK,EAAE,CAChB,GAAM,EAAE,WACV,CACD,YAAa,EAAO,SACZ,EACL,GAAM,EAAE,aAAe,EACzB,CACD,aAAc,EACd,WAAY,EACb,CAAC,CAOJ,WAA6B,CAC3B,IAAM,EAAQ,KAAK,MAAM,UAAU,CAGnC,MAAO,CACL,MAHW,KAAK,MAAM,MAAM,CAI5B,MAAO,EAAM,KACb,eAAgB,EAAM,QACtB,mBAAoB,EAAM,mBAC3B,CAMH,OAA8B,CAU5B,OATA,EAAI,sBAAsB,CAG1B,KAAK,MAAM,OAAO,CAGlB,KAAK,SAAW,EAAO,MAAM,CAC7B,KAAK,YAAc,GACnB,EAAI,kBAAkB,CACf,EAAQ,IAAA,GAAU"}
@@ -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"}
@@ -1,75 +1,113 @@
1
- import { List } from 'functype/list';
2
- import { Option } from 'functype/option';
3
- import { UserDatabase } from '../types.js';
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 for managing active user database connections
19
+ * Functype-compatible LRU Cache wrapper
9
20
  *
10
- * Uses functype Option for safe null handling
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
- private cache;
14
- private readonly maxSize;
15
- constructor(maxSize?: number);
16
- /**
17
- * Get a value from the cache
18
- * Returns Option.Some(value) if found, Option.None otherwise
19
- */
20
- get(key: string): Option<T>;
21
- /**
22
- * Set a value in the cache
23
- * Evicts LRU item if at capacity
24
- */
25
- set(key: string, value: T): void;
26
- /**
27
- * Remove a value from the cache
28
- */
29
- delete(key: string): boolean;
30
- /**
31
- * Check if a key exists in the cache
32
- */
33
- has(key: string): boolean;
34
- /**
35
- * Get the least recently used key
36
- * Returns Option.Some(key) if cache not empty, Option.None otherwise
37
- */
38
- getLRU(): Option<string>;
39
- /**
40
- * Evict the least recently used item
41
- */
42
- private evictLRU;
43
- /**
44
- * Get all keys for items older than the timeout
45
- * Returns a List of stale keys (functype immutable list)
46
- */
47
- getStale(timeoutMs: number): List<string>;
48
- /**
49
- * Get the current size of the cache
50
- */
51
- size(): number;
52
- /**
53
- * Clear all items from the cache
54
- */
55
- clear(): void;
56
- /**
57
- * Get all values as a List (functype immutable list)
58
- */
59
- values(): List<T>;
60
- /**
61
- * Get all keys as a List
62
- */
63
- keys(): List<string>;
64
- /**
65
- * Get cache statistics
66
- */
67
- getStats(): {
68
- size: number;
69
- maxSize: number;
70
- utilizationPercent: number;
71
- oldestAccessTime: Option<Date>;
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
@@ -1,2 +1 @@
1
- "use strict";Object.defineProperty(exports, "__esModule", {value: true});var _chunkJ2OQ62DVjs = require('../chunk-J2OQ62DV.js');require('../chunk-Q6UFPTQC.js');require('../chunk-5XGN7UAV.js');exports.LRUCache = _chunkJ2OQ62DVjs.a;
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
- export { DuckPond } from './DuckPond.js';
2
- export { AsyncDuckPondResult, ColumnSchema, CreateUserOptions, DuckPondConfig, DuckPondError, DuckPondEvent, DuckPondMetrics, DuckPondResult, ErrorCode, FileInfo, ListUsersResult, QueryResult, ResolvedConfig, Schema, StorageStats, TableSchema, UserDatabase, UserStats } from './types.js';
3
- export { Errors, createError, formatError, success, toDuckPondError } from './utils/errors.js';
4
- export { createLogger, loggers } from './utils/logger.js';
5
- export { LRUCache } from './cache/LRUCache.js';
6
- export { Either, Left, Right } from 'functype/either';
7
- export { List } from 'functype/list';
8
- export { Option } from 'functype/option';
9
- export { Try } from 'functype/try';
10
- import '@duckdb/node-api';
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
- "use strict";Object.defineProperty(exports, "__esModule", {value: true});var _chunkMLZHFU2Bjs = require('./chunk-MLZHFU2B.js');var _chunkJ2OQ62DVjs = require('./chunk-J2OQ62DV.js');var _chunkMZTKR3LRjs = require('./chunk-MZTKR3LR.js');var _chunk2PPSIE57js = require('./chunk-2PPSIE57.js');var _chunkQ6UFPTQCjs = require('./chunk-Q6UFPTQC.js');require('./chunk-5XGN7UAV.js');var _either = require('functype/either');var _list = require('functype/list');var _option = require('functype/option');var _try = require('functype/try');exports.DuckPond = _chunkMLZHFU2Bjs.a; exports.Either = _either.Either; exports.ErrorCode = _chunk2PPSIE57js.a; exports.Errors = _chunkMZTKR3LRjs.d; exports.LRUCache = _chunkJ2OQ62DVjs.a; exports.Left = _either.Left; exports.List = _list.List; exports.Option = _option.Option; exports.Right = _either.Right; exports.Try = _try.Try; exports.createError = _chunkMZTKR3LRjs.a; exports.createLogger = _chunkQ6UFPTQCjs.a; exports.formatError = _chunkMZTKR3LRjs.e; exports.loggers = _chunkQ6UFPTQCjs.b; exports.success = _chunkMZTKR3LRjs.b; exports.toDuckPondError = _chunkMZTKR3LRjs.c;
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};