duckpond 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +453 -0
- package/dist/DuckPond.d.mts +94 -0
- package/dist/DuckPond.d.ts +94 -0
- package/dist/DuckPond.js +2 -0
- package/dist/DuckPond.js.map +1 -0
- package/dist/DuckPond.mjs +2 -0
- package/dist/DuckPond.mjs.map +1 -0
- package/dist/cache/LRUCache.d.mts +75 -0
- package/dist/cache/LRUCache.d.ts +75 -0
- package/dist/cache/LRUCache.js +2 -0
- package/dist/cache/LRUCache.js.map +1 -0
- package/dist/cache/LRUCache.mjs +2 -0
- package/dist/cache/LRUCache.mjs.map +1 -0
- package/dist/chunk-24M54WUC.mjs +2 -0
- package/dist/chunk-24M54WUC.mjs.map +1 -0
- package/dist/chunk-4NKFJCEP.mjs +27 -0
- package/dist/chunk-4NKFJCEP.mjs.map +1 -0
- package/dist/chunk-5XGN7UAV.js +2 -0
- package/dist/chunk-5XGN7UAV.js.map +1 -0
- package/dist/chunk-DTZ5B6AO.mjs +2 -0
- package/dist/chunk-DTZ5B6AO.mjs.map +1 -0
- package/dist/chunk-E5ZZH3QB.js +2 -0
- package/dist/chunk-E5ZZH3QB.js.map +1 -0
- package/dist/chunk-J2OQ62DV.js +2 -0
- package/dist/chunk-J2OQ62DV.js.map +1 -0
- package/dist/chunk-MZTKR3LR.js +3 -0
- package/dist/chunk-MZTKR3LR.js.map +1 -0
- package/dist/chunk-PCQEPXO3.mjs +3 -0
- package/dist/chunk-PCQEPXO3.mjs.map +1 -0
- package/dist/chunk-Q6UFPTQC.js +2 -0
- package/dist/chunk-Q6UFPTQC.js.map +1 -0
- package/dist/chunk-SZJXSB7U.mjs +2 -0
- package/dist/chunk-SZJXSB7U.mjs.map +1 -0
- package/dist/chunk-TLGHSO3F.js +27 -0
- package/dist/chunk-TLGHSO3F.js.map +1 -0
- package/dist/chunk-V57JCP3U.mjs +2 -0
- package/dist/chunk-V57JCP3U.mjs.map +1 -0
- package/dist/index.d.mts +12 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +2 -0
- package/dist/index.mjs.map +1 -0
- package/dist/types.d.mts +182 -0
- package/dist/types.d.ts +182 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/types.mjs +2 -0
- package/dist/types.mjs.map +1 -0
- package/dist/utils/errors.d.mts +40 -0
- package/dist/utils/errors.d.ts +40 -0
- package/dist/utils/errors.js +2 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/errors.mjs +2 -0
- package/dist/utils/errors.mjs.map +1 -0
- package/dist/utils/logger.d.mts +25 -0
- package/dist/utils/logger.d.ts +25 -0
- package/dist/utils/logger.js +2 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/logger.mjs +2 -0
- package/dist/utils/logger.mjs.map +1 -0
- package/package.json +80 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Jordan
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
# DuckPond
|
|
2
|
+
|
|
3
|
+
[](https://github.com/jordanburke/duckpond/actions/workflows/node.js.yml)
|
|
4
|
+
[](https://github.com/jordanburke/duckpond/actions/workflows/codeql.yml)
|
|
5
|
+
|
|
6
|
+
Multi-tenant DuckDB manager with R2/S3 storage and functional programming patterns.
|
|
7
|
+
|
|
8
|
+
## Features
|
|
9
|
+
|
|
10
|
+
- 🏢 **Multi-Tenant Isolation** - Per-user database instances with automatic resource management
|
|
11
|
+
- ☁️ **Cloud Storage** - Native Cloudflare R2 and AWS S3 integration
|
|
12
|
+
- 🛡️ **Type-Safe Functional Programming** - Built with [functype](https://github.com/jordanburke/functype) for robust error handling
|
|
13
|
+
- 🚀 **LRU Caching** - Intelligent caching with automatic eviction of idle users
|
|
14
|
+
- 📊 **Storage Strategies** - Flexible parquet, duckdb, or hybrid storage options
|
|
15
|
+
- 🔧 **TypeScript-First** - Full type safety with comprehensive TypeScript declarations
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install duckpond
|
|
21
|
+
# or
|
|
22
|
+
pnpm add duckpond
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Quick Start
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
import { DuckPond } from "duckpond"
|
|
29
|
+
|
|
30
|
+
// Configure with Cloudflare R2
|
|
31
|
+
const pond = new DuckPond({
|
|
32
|
+
r2: {
|
|
33
|
+
accountId: process.env.R2_ACCOUNT_ID!,
|
|
34
|
+
accessKeyId: process.env.R2_ACCESS_KEY_ID!,
|
|
35
|
+
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY!,
|
|
36
|
+
bucket: "my-bucket",
|
|
37
|
+
},
|
|
38
|
+
maxActiveUsers: 10,
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
// Initialize
|
|
42
|
+
await pond.init()
|
|
43
|
+
|
|
44
|
+
// Query with functional error handling
|
|
45
|
+
const result = await pond.query("user123", "SELECT * FROM orders")
|
|
46
|
+
result.fold(
|
|
47
|
+
(error) => console.error("Query failed:", error.message),
|
|
48
|
+
(rows) => console.log("Results:", rows),
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
// Cleanup
|
|
52
|
+
await pond.close()
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Core Concepts
|
|
56
|
+
|
|
57
|
+
### Functional Error Handling
|
|
58
|
+
|
|
59
|
+
DuckPond uses [functype](https://github.com/jordanburke/functype) for type-safe error handling without exceptions:
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
import { Either } from "duckpond"
|
|
63
|
+
|
|
64
|
+
// All operations return Either<Error, Success>
|
|
65
|
+
const result = await pond.query<{ id: number; name: string }>("user123", "SELECT * FROM users")
|
|
66
|
+
|
|
67
|
+
// Pattern match on success/failure
|
|
68
|
+
result.fold(
|
|
69
|
+
(error) => {
|
|
70
|
+
// Handle error case
|
|
71
|
+
console.error(`[${error.code}] ${error.message}`)
|
|
72
|
+
if (error.cause) console.error("Caused by:", error.cause)
|
|
73
|
+
},
|
|
74
|
+
(rows) => {
|
|
75
|
+
// Handle success case
|
|
76
|
+
rows.forEach((user) => console.log(`${user.id}: ${user.name}`))
|
|
77
|
+
},
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
// Or check explicitly
|
|
81
|
+
if (result.isLeft()) {
|
|
82
|
+
const error = result.fold(
|
|
83
|
+
(err) => err,
|
|
84
|
+
() => null,
|
|
85
|
+
)
|
|
86
|
+
// Handle error
|
|
87
|
+
} else {
|
|
88
|
+
const rows = result.fold(
|
|
89
|
+
() => [],
|
|
90
|
+
(data) => data,
|
|
91
|
+
)
|
|
92
|
+
// Process rows
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Multi-Tenant Isolation
|
|
97
|
+
|
|
98
|
+
Each user gets an isolated database instance:
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
// User A's queries don't affect User B
|
|
102
|
+
await pond.query("userA", "CREATE TABLE orders (id INT)")
|
|
103
|
+
await pond.query("userB", "SELECT * FROM orders") // Error: table doesn't exist
|
|
104
|
+
|
|
105
|
+
// Check user status
|
|
106
|
+
const isActive = pond.isAttached("userA") // true if cached
|
|
107
|
+
|
|
108
|
+
// Get user statistics
|
|
109
|
+
const stats = await pond.getUserStats("userA")
|
|
110
|
+
stats.fold(
|
|
111
|
+
(error) => console.error(error.message),
|
|
112
|
+
(info) => console.log(`User: ${info.userId}, Last access: ${info.lastAccess}`),
|
|
113
|
+
)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Storage Strategies
|
|
117
|
+
|
|
118
|
+
DuckPond supports multiple storage strategies:
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
// Parquet files (default) - best for analytics
|
|
122
|
+
const pond = new DuckPond({
|
|
123
|
+
r2: {
|
|
124
|
+
/* ... */
|
|
125
|
+
},
|
|
126
|
+
strategy: "parquet",
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
// DuckDB files - full database persistence
|
|
130
|
+
const pond = new DuckPond({
|
|
131
|
+
r2: {
|
|
132
|
+
/* ... */
|
|
133
|
+
},
|
|
134
|
+
strategy: "duckdb",
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
// Hybrid - mix both approaches
|
|
138
|
+
const pond = new DuckPond({
|
|
139
|
+
r2: {
|
|
140
|
+
/* ... */
|
|
141
|
+
},
|
|
142
|
+
strategy: "hybrid",
|
|
143
|
+
})
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## API Reference
|
|
147
|
+
|
|
148
|
+
### DuckPond Class
|
|
149
|
+
|
|
150
|
+
#### `constructor(config: DuckPondConfig)`
|
|
151
|
+
|
|
152
|
+
Creates a new DuckPond instance.
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
const pond = new DuckPond({
|
|
156
|
+
// R2 Configuration (Cloudflare)
|
|
157
|
+
r2: {
|
|
158
|
+
accountId: string
|
|
159
|
+
accessKeyId: string
|
|
160
|
+
secretAccessKey: string
|
|
161
|
+
bucket: string
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
// OR S3 Configuration (AWS)
|
|
165
|
+
s3: {
|
|
166
|
+
region: string
|
|
167
|
+
accessKeyId: string
|
|
168
|
+
secretAccessKey: string
|
|
169
|
+
bucket: string
|
|
170
|
+
endpoint?: string // For S3-compatible services
|
|
171
|
+
},
|
|
172
|
+
|
|
173
|
+
// Optional settings
|
|
174
|
+
memoryLimit: '4GB', // DuckDB memory limit
|
|
175
|
+
threads: 4, // Number of threads
|
|
176
|
+
maxActiveUsers: 10, // LRU cache size
|
|
177
|
+
evictionTimeout: 300000, // Idle timeout (5 min)
|
|
178
|
+
cacheType: 'disk', // 'disk' | 'memory' | 'noop'
|
|
179
|
+
strategy: 'parquet' // 'parquet' | 'duckdb' | 'hybrid'
|
|
180
|
+
})
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
#### `async init(): AsyncDuckPondResult<void>`
|
|
184
|
+
|
|
185
|
+
Initialize DuckPond. Must be called before any operations.
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
const result = await pond.init()
|
|
189
|
+
result.fold(
|
|
190
|
+
(error) => console.error("Initialization failed:", error.message),
|
|
191
|
+
() => console.log("Ready!"),
|
|
192
|
+
)
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
#### `async query<T>(userId: string, sql: string): AsyncDuckPondResult<T[]>`
|
|
196
|
+
|
|
197
|
+
Execute a SQL query for a specific user.
|
|
198
|
+
|
|
199
|
+
```typescript
|
|
200
|
+
const result = await pond.query<{ id: number; total: number }>(
|
|
201
|
+
"user123",
|
|
202
|
+
"SELECT id, SUM(amount) as total FROM orders GROUP BY id",
|
|
203
|
+
)
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
#### `async execute(userId: string, sql: string): AsyncDuckPondResult<void>`
|
|
207
|
+
|
|
208
|
+
Execute SQL without returning results (DDL, DML).
|
|
209
|
+
|
|
210
|
+
```typescript
|
|
211
|
+
await pond.execute(
|
|
212
|
+
"user123",
|
|
213
|
+
`
|
|
214
|
+
CREATE TABLE products (
|
|
215
|
+
id INTEGER PRIMARY KEY,
|
|
216
|
+
name VARCHAR,
|
|
217
|
+
price DECIMAL(10,2)
|
|
218
|
+
)
|
|
219
|
+
`,
|
|
220
|
+
)
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
#### `async getUserStats(userId: string): AsyncDuckPondResult<UserStats>`
|
|
224
|
+
|
|
225
|
+
Get statistics about a user's database.
|
|
226
|
+
|
|
227
|
+
```typescript
|
|
228
|
+
const result = await pond.getUserStats("user123")
|
|
229
|
+
result.fold(
|
|
230
|
+
(error) => console.error(error.message),
|
|
231
|
+
(stats) =>
|
|
232
|
+
console.log({
|
|
233
|
+
userId: stats.userId,
|
|
234
|
+
attached: stats.attached,
|
|
235
|
+
lastAccess: stats.lastAccess,
|
|
236
|
+
memoryUsage: stats.memoryUsage,
|
|
237
|
+
}),
|
|
238
|
+
)
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
#### `isAttached(userId: string): boolean`
|
|
242
|
+
|
|
243
|
+
Check if a user is currently cached.
|
|
244
|
+
|
|
245
|
+
```typescript
|
|
246
|
+
if (pond.isAttached("user123")) {
|
|
247
|
+
console.log("User database is active")
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
#### `async detachUser(userId: string): AsyncDuckPondResult<void>`
|
|
252
|
+
|
|
253
|
+
Manually detach a user's database from the cache.
|
|
254
|
+
|
|
255
|
+
```typescript
|
|
256
|
+
await pond.detachUser("user123")
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
#### `async close(): AsyncDuckPondResult<void>`
|
|
260
|
+
|
|
261
|
+
Close DuckPond and cleanup all resources.
|
|
262
|
+
|
|
263
|
+
```typescript
|
|
264
|
+
await pond.close()
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### Error Codes
|
|
268
|
+
|
|
269
|
+
```typescript
|
|
270
|
+
import { ErrorCode } from "duckpond"
|
|
271
|
+
|
|
272
|
+
ErrorCode.CONNECTION_FAILED
|
|
273
|
+
ErrorCode.R2_CONNECTION_ERROR
|
|
274
|
+
ErrorCode.S3_CONNECTION_ERROR
|
|
275
|
+
ErrorCode.USER_NOT_FOUND
|
|
276
|
+
ErrorCode.QUERY_EXECUTION_ERROR
|
|
277
|
+
ErrorCode.QUERY_TIMEOUT
|
|
278
|
+
ErrorCode.MEMORY_LIMIT_EXCEEDED
|
|
279
|
+
ErrorCode.STORAGE_ERROR
|
|
280
|
+
ErrorCode.INVALID_CONFIG
|
|
281
|
+
ErrorCode.NOT_INITIALIZED
|
|
282
|
+
ErrorCode.UNKNOWN_ERROR
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
## Examples
|
|
286
|
+
|
|
287
|
+
### AWS S3 Configuration
|
|
288
|
+
|
|
289
|
+
```typescript
|
|
290
|
+
const pond = new DuckPond({
|
|
291
|
+
s3: {
|
|
292
|
+
region: "us-east-1",
|
|
293
|
+
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
|
|
294
|
+
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
|
|
295
|
+
bucket: "my-duckdb-bucket",
|
|
296
|
+
},
|
|
297
|
+
})
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
### MinIO or S3-Compatible Storage
|
|
301
|
+
|
|
302
|
+
```typescript
|
|
303
|
+
const pond = new DuckPond({
|
|
304
|
+
s3: {
|
|
305
|
+
region: "us-east-1",
|
|
306
|
+
accessKeyId: "minioadmin",
|
|
307
|
+
secretAccessKey: "minioadmin",
|
|
308
|
+
bucket: "duckdb",
|
|
309
|
+
endpoint: "http://localhost:9000",
|
|
310
|
+
},
|
|
311
|
+
})
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
### Advanced Error Handling
|
|
315
|
+
|
|
316
|
+
```typescript
|
|
317
|
+
import { ErrorCode } from "duckpond"
|
|
318
|
+
|
|
319
|
+
const result = await pond.query("user123", "SELECT * FROM orders")
|
|
320
|
+
|
|
321
|
+
result.fold(
|
|
322
|
+
(error) => {
|
|
323
|
+
switch (error.code) {
|
|
324
|
+
case ErrorCode.QUERY_EXECUTION_ERROR:
|
|
325
|
+
console.error("SQL error:", error.message)
|
|
326
|
+
if (error.context?.sql) {
|
|
327
|
+
console.error("Query:", error.context.sql)
|
|
328
|
+
}
|
|
329
|
+
break
|
|
330
|
+
|
|
331
|
+
case ErrorCode.USER_NOT_FOUND:
|
|
332
|
+
console.error("User not found:", error.context?.userId)
|
|
333
|
+
break
|
|
334
|
+
|
|
335
|
+
case ErrorCode.MEMORY_LIMIT_EXCEEDED:
|
|
336
|
+
console.error("Out of memory:", error.context?.limit)
|
|
337
|
+
break
|
|
338
|
+
|
|
339
|
+
default:
|
|
340
|
+
console.error("Unexpected error:", error)
|
|
341
|
+
}
|
|
342
|
+
},
|
|
343
|
+
(rows) => {
|
|
344
|
+
console.log(`Fetched ${rows.length} rows`)
|
|
345
|
+
},
|
|
346
|
+
)
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
### Using Functype Utilities
|
|
350
|
+
|
|
351
|
+
```typescript
|
|
352
|
+
import { Option, List } from "duckpond"
|
|
353
|
+
|
|
354
|
+
// Safe null handling with Option
|
|
355
|
+
const maybeUser = Option(user)
|
|
356
|
+
const userName = maybeUser.map((u) => u.name).orElse("Anonymous")
|
|
357
|
+
|
|
358
|
+
// Immutable collections with List
|
|
359
|
+
const users = List([
|
|
360
|
+
{ id: 1, name: "Alice" },
|
|
361
|
+
{ id: 2, name: "Bob" },
|
|
362
|
+
])
|
|
363
|
+
|
|
364
|
+
const names = users
|
|
365
|
+
.map((u) => u.name)
|
|
366
|
+
.filter((name) => name.startsWith("A"))
|
|
367
|
+
.toArray()
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
## Development
|
|
371
|
+
|
|
372
|
+
### Pre-Checkin Command
|
|
373
|
+
|
|
374
|
+
```bash
|
|
375
|
+
pnpm validate # 🚀 Format, lint, test, and build
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
### Individual Commands
|
|
379
|
+
|
|
380
|
+
```bash
|
|
381
|
+
# Formatting
|
|
382
|
+
pnpm format # Format code with Prettier
|
|
383
|
+
pnpm format:check # Check formatting without writing
|
|
384
|
+
|
|
385
|
+
# Linting
|
|
386
|
+
pnpm lint # Fix ESLint issues
|
|
387
|
+
pnpm lint:check # Check ESLint issues without fixing
|
|
388
|
+
|
|
389
|
+
# Testing
|
|
390
|
+
pnpm test # Run tests once
|
|
391
|
+
pnpm test:watch # Run tests in watch mode
|
|
392
|
+
pnpm test:coverage # Run tests with coverage
|
|
393
|
+
pnpm test:ui # Launch Vitest UI
|
|
394
|
+
|
|
395
|
+
# Building
|
|
396
|
+
pnpm build # Production build
|
|
397
|
+
pnpm dev # Development mode with watch
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
## Architecture
|
|
401
|
+
|
|
402
|
+
```
|
|
403
|
+
┌─────────────────────────────────────────┐
|
|
404
|
+
│ DuckPond Manager │
|
|
405
|
+
│ - User isolation & lifecycle │
|
|
406
|
+
│ - Connection pooling │
|
|
407
|
+
│ - Functional error handling │
|
|
408
|
+
└──────────────┬──────────────────────────┘
|
|
409
|
+
│
|
|
410
|
+
┌───────┴────────┐
|
|
411
|
+
│ LRU Cache │
|
|
412
|
+
│ - Max active │
|
|
413
|
+
│ - Auto-evict │
|
|
414
|
+
└───────┬────────┘
|
|
415
|
+
│
|
|
416
|
+
┌───────▼────────┐
|
|
417
|
+
│ DuckDB Inst │
|
|
418
|
+
│ - Per-user DB │
|
|
419
|
+
│ - R2/S3 mount │
|
|
420
|
+
└───────┬────────┘
|
|
421
|
+
│
|
|
422
|
+
┌───────▼────────┐
|
|
423
|
+
│ Cloud Storage │
|
|
424
|
+
│ - R2 / S3 │
|
|
425
|
+
│ - Parquet files│
|
|
426
|
+
└────────────────┘
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
### Key Components
|
|
430
|
+
|
|
431
|
+
- **DuckPond**: Main manager class handling user lifecycle and queries
|
|
432
|
+
- **LRUCache**: Generic LRU cache with functype Option/List integration
|
|
433
|
+
- **Error Utilities**: Functional error creation and handling with Either
|
|
434
|
+
- **Types**: Comprehensive TypeScript definitions for all APIs
|
|
435
|
+
|
|
436
|
+
## Contributing
|
|
437
|
+
|
|
438
|
+
Contributions are welcome! Please ensure:
|
|
439
|
+
|
|
440
|
+
1. All tests pass: `pnpm test`
|
|
441
|
+
2. Code is formatted: `pnpm format`
|
|
442
|
+
3. No lint errors: `pnpm lint:check`
|
|
443
|
+
4. Build succeeds: `pnpm build`
|
|
444
|
+
|
|
445
|
+
Or simply run: `pnpm validate`
|
|
446
|
+
|
|
447
|
+
## License
|
|
448
|
+
|
|
449
|
+
MIT - see LICENSE file for details
|
|
450
|
+
|
|
451
|
+
---
|
|
452
|
+
|
|
453
|
+
Built with [functype](https://github.com/jordanburke/functype) for functional TypeScript
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { DuckDBConnection } from '@duckdb/node-api';
|
|
2
|
+
import { DuckPondConfig, AsyncDuckPondResult, UserStats } from './types.mjs';
|
|
3
|
+
import 'functype/either';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* DuckPond - Multi-tenant DuckDB manager with R2/S3 storage
|
|
7
|
+
*
|
|
8
|
+
* Manages per-user DuckDB instances with:
|
|
9
|
+
* - LRU caching for active users
|
|
10
|
+
* - R2/S3 object storage integration
|
|
11
|
+
* - Functional error handling with functype Either
|
|
12
|
+
* - Automatic resource cleanup
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```typescript
|
|
16
|
+
* const pond = new DuckPond({
|
|
17
|
+
* r2: {
|
|
18
|
+
* accountId: 'xxx',
|
|
19
|
+
* accessKeyId: 'yyy',
|
|
20
|
+
* secretAccessKey: 'zzz',
|
|
21
|
+
* bucket: 'my-bucket'
|
|
22
|
+
* }
|
|
23
|
+
* })
|
|
24
|
+
*
|
|
25
|
+
* await pond.init()
|
|
26
|
+
*
|
|
27
|
+
* const result = await pond.query('user123', 'SELECT * FROM orders')
|
|
28
|
+
* result.fold(
|
|
29
|
+
* error => console.error('Query failed:', error),
|
|
30
|
+
* rows => console.log('Results:', rows)
|
|
31
|
+
* )
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
declare class DuckPond {
|
|
35
|
+
private instance;
|
|
36
|
+
private cache;
|
|
37
|
+
private config;
|
|
38
|
+
private evictionTimer;
|
|
39
|
+
private initialized;
|
|
40
|
+
constructor(config: DuckPondConfig);
|
|
41
|
+
/**
|
|
42
|
+
* Initialize DuckPond
|
|
43
|
+
* Must be called before any other operations
|
|
44
|
+
*/
|
|
45
|
+
init(): AsyncDuckPondResult<void>;
|
|
46
|
+
/**
|
|
47
|
+
* Configure R2/S3 access and DuckDB extensions
|
|
48
|
+
*/
|
|
49
|
+
private setupCloudStorage;
|
|
50
|
+
/**
|
|
51
|
+
* Get a connection for a user
|
|
52
|
+
* Loads from cache or attaches new database
|
|
53
|
+
*/
|
|
54
|
+
getUserConnection(userId: string): AsyncDuckPondResult<DuckDBConnection>;
|
|
55
|
+
/**
|
|
56
|
+
* Attach a user's database based on storage strategy
|
|
57
|
+
*/
|
|
58
|
+
private attachUserDatabase;
|
|
59
|
+
/**
|
|
60
|
+
* Execute a SQL query for a user
|
|
61
|
+
* Returns Either<Error, results>
|
|
62
|
+
*/
|
|
63
|
+
query<T = unknown>(userId: string, sql: string): AsyncDuckPondResult<T[]>;
|
|
64
|
+
/**
|
|
65
|
+
* Execute SQL without returning results (DDL, DML)
|
|
66
|
+
*/
|
|
67
|
+
execute(userId: string, sql: string): AsyncDuckPondResult<void>;
|
|
68
|
+
/**
|
|
69
|
+
* Detach a user's database and free resources
|
|
70
|
+
*/
|
|
71
|
+
detachUser(userId: string): AsyncDuckPondResult<void>;
|
|
72
|
+
/**
|
|
73
|
+
* Evict the least recently used user
|
|
74
|
+
*/
|
|
75
|
+
private evictLRU;
|
|
76
|
+
/**
|
|
77
|
+
* Start background timer to evict idle users
|
|
78
|
+
*/
|
|
79
|
+
private startEvictionTimer;
|
|
80
|
+
/**
|
|
81
|
+
* Check if a user is currently attached
|
|
82
|
+
*/
|
|
83
|
+
isAttached(userId: string): boolean;
|
|
84
|
+
/**
|
|
85
|
+
* Get statistics about a user's database
|
|
86
|
+
*/
|
|
87
|
+
getUserStats(userId: string): AsyncDuckPondResult<UserStats>;
|
|
88
|
+
/**
|
|
89
|
+
* Close DuckPond and cleanup all resources
|
|
90
|
+
*/
|
|
91
|
+
close(): AsyncDuckPondResult<void>;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export { DuckPond };
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { DuckDBConnection } from '@duckdb/node-api';
|
|
2
|
+
import { DuckPondConfig, AsyncDuckPondResult, UserStats } from './types.js';
|
|
3
|
+
import 'functype/either';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* DuckPond - Multi-tenant DuckDB manager with R2/S3 storage
|
|
7
|
+
*
|
|
8
|
+
* Manages per-user DuckDB instances with:
|
|
9
|
+
* - LRU caching for active users
|
|
10
|
+
* - R2/S3 object storage integration
|
|
11
|
+
* - Functional error handling with functype Either
|
|
12
|
+
* - Automatic resource cleanup
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```typescript
|
|
16
|
+
* const pond = new DuckPond({
|
|
17
|
+
* r2: {
|
|
18
|
+
* accountId: 'xxx',
|
|
19
|
+
* accessKeyId: 'yyy',
|
|
20
|
+
* secretAccessKey: 'zzz',
|
|
21
|
+
* bucket: 'my-bucket'
|
|
22
|
+
* }
|
|
23
|
+
* })
|
|
24
|
+
*
|
|
25
|
+
* await pond.init()
|
|
26
|
+
*
|
|
27
|
+
* const result = await pond.query('user123', 'SELECT * FROM orders')
|
|
28
|
+
* result.fold(
|
|
29
|
+
* error => console.error('Query failed:', error),
|
|
30
|
+
* rows => console.log('Results:', rows)
|
|
31
|
+
* )
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
declare class DuckPond {
|
|
35
|
+
private instance;
|
|
36
|
+
private cache;
|
|
37
|
+
private config;
|
|
38
|
+
private evictionTimer;
|
|
39
|
+
private initialized;
|
|
40
|
+
constructor(config: DuckPondConfig);
|
|
41
|
+
/**
|
|
42
|
+
* Initialize DuckPond
|
|
43
|
+
* Must be called before any other operations
|
|
44
|
+
*/
|
|
45
|
+
init(): AsyncDuckPondResult<void>;
|
|
46
|
+
/**
|
|
47
|
+
* Configure R2/S3 access and DuckDB extensions
|
|
48
|
+
*/
|
|
49
|
+
private setupCloudStorage;
|
|
50
|
+
/**
|
|
51
|
+
* Get a connection for a user
|
|
52
|
+
* Loads from cache or attaches new database
|
|
53
|
+
*/
|
|
54
|
+
getUserConnection(userId: string): AsyncDuckPondResult<DuckDBConnection>;
|
|
55
|
+
/**
|
|
56
|
+
* Attach a user's database based on storage strategy
|
|
57
|
+
*/
|
|
58
|
+
private attachUserDatabase;
|
|
59
|
+
/**
|
|
60
|
+
* Execute a SQL query for a user
|
|
61
|
+
* Returns Either<Error, results>
|
|
62
|
+
*/
|
|
63
|
+
query<T = unknown>(userId: string, sql: string): AsyncDuckPondResult<T[]>;
|
|
64
|
+
/**
|
|
65
|
+
* Execute SQL without returning results (DDL, DML)
|
|
66
|
+
*/
|
|
67
|
+
execute(userId: string, sql: string): AsyncDuckPondResult<void>;
|
|
68
|
+
/**
|
|
69
|
+
* Detach a user's database and free resources
|
|
70
|
+
*/
|
|
71
|
+
detachUser(userId: string): AsyncDuckPondResult<void>;
|
|
72
|
+
/**
|
|
73
|
+
* Evict the least recently used user
|
|
74
|
+
*/
|
|
75
|
+
private evictLRU;
|
|
76
|
+
/**
|
|
77
|
+
* Start background timer to evict idle users
|
|
78
|
+
*/
|
|
79
|
+
private startEvictionTimer;
|
|
80
|
+
/**
|
|
81
|
+
* Check if a user is currently attached
|
|
82
|
+
*/
|
|
83
|
+
isAttached(userId: string): boolean;
|
|
84
|
+
/**
|
|
85
|
+
* Get statistics about a user's database
|
|
86
|
+
*/
|
|
87
|
+
getUserStats(userId: string): AsyncDuckPondResult<UserStats>;
|
|
88
|
+
/**
|
|
89
|
+
* Close DuckPond and cleanup all resources
|
|
90
|
+
*/
|
|
91
|
+
close(): AsyncDuckPondResult<void>;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export { DuckPond };
|
package/dist/DuckPond.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true});var _chunkTLGHSO3Fjs = require('./chunk-TLGHSO3F.js');require('./chunk-J2OQ62DV.js');require('./chunk-MZTKR3LR.js');require('./chunk-E5ZZH3QB.js');require('./chunk-Q6UFPTQC.js');require('./chunk-5XGN7UAV.js');exports.DuckPond = _chunkTLGHSO3Fjs.a;
|
|
2
|
+
//# sourceMappingURL=DuckPond.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["/home/jordanburke/IdeaProjects/duckpond/dist/DuckPond.js"],"names":[],"mappings":"AAAA,+HAAkC,+BAA4B,+BAA4B,+BAA4B,+BAA4B,+BAA4B,sCAAsB","file":"/home/jordanburke/IdeaProjects/duckpond/dist/DuckPond.js"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|